django中间件CsrfViewMiddleware源码分析,探究csrf实现
Django Documentation
csrf保护基于以下:
1. 一个CSRF cookie 基于一个随机生成的值,其他网站无法得到。此cookie由CsrfViewMiddleware
产生。它与每个调用django.middleware.csrf.get_token()
(这是一个用于取回CSRF token的方法)的响应一起发送,如果它尚未在请求上设置的话。
为了防止BREACH攻击,token
不仅仅是秘密;随机的salt
被置于secret
之前并用来加密它。出于安全原因,每次用户登录时都会更改密钥的值。
所有传出POST表单中都有一个名为
csrfmiddlewaretoken
的隐藏表单字段。此字段的值同样是秘密的值。salt添加到它并用于加扰它。每次调用get_token()时都会重新生成salt,以便在每个此类响应中更改表单字段值。这部分由template的{% csrf_token %}
完成。对于未使用
HTTP
GET
,HEAD
,OPTIONS
或TRACE
的所有传入请求,必须带有CSRF cookie
,并且csrfmiddlewaretoken
字段必须存在且正确。如果不是,用户将收到403错误。
验证csrfmiddlewaretoken
字段值时,只将secret而不是整个token与cookie值中的secret
进行比较。这允许使用不断变化的token
。虽然每个请求都可以使用自己的token
,但secret
仍然是所有人共同的。
此检查由CsrfViewMiddleware
完成。此外,对于
HTTPS
请求,严格的引用检查由CsrfViewMiddleware
完成。这意味着即使子域可以在您的域上设置或修改cookie
,它也不能强制用户发布到您的应用程序,因为该请求不会来自您自己的确切域。 这也解决了在使用会话独立秘密时在HTTPS下可能发生的中间人攻击,因为即使在HTTPS下与站点通信时,HTTP Set-Cookie标头(不幸)也被客户接受了。 。 (对HTTP请求不进行引用检查,因为在HTTP下,Referer头的存在不够可靠。) 如果设置了CSRF_COOKIE_DOMAIN
设置,则会将引用者与其进行比较。此设置支持子域。例如,CSRF_COOKIE_DOMAIN ='.example.com'
将允许来自www.example.com
和api.example.com
的POST请求。如果未设置该设置,则referer
必须与HTTP Host标头匹配。 可以使用CSRF_TRUSTED_ORIGINS
设置将已接受的引用扩展到当前主机或cookie域之外。
流程图
CsrfViewMiddleware.process_request
# django/middleware/csrf.py
class CsrfViewMiddleware(MiddlewareMixin):
def process_request(self, request):
csrf_token = self._get_token(request)
# 第一次访问,csrf_token返回None,
if csrf_token is not None:
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
# request.META 是一个 Python 字典,包含了所有本次 HTTP 请求的 Header
# 信息,比如用户 IP 地址和用户Agent(通常是浏览器的名称和版本号)。
settings = LazySettings()
方法_get_token
,从名字上来看就是获取token,_get_token
在后面多处地方都有用到
# django/middleware/csrf.py
def _get_token(self, request):
# CSRF_USE_SESSIONS在django/conf/global_settings.py,默认为False,执行else
if settings.CSRF_USE_SESSIONS:
try:
return request.session.get(CSRF_SESSION_KEY)
except AttributeError:
raise ImproperlyConfigured(
'CSRF_USE_SESSIONS is enabled, but request.session is not '
'set. SessionMiddleware must appear before CsrfViewMiddleware '
'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
)
else:
try:
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
# CSRF_SESSION_KEY= "csrftoken"
except KeyError:
# 第一次访问的时候 request.COOKIES = {},所以直接返回
return None
csrf_token = _sanitize_token(cookie_token)
# csrf 对不上 cookie里 的 token,标记csrf_cookie_needs_reset=True,
# 在process_response的方法中判定
if csrf_token != cookie_token:
# Cookie token needed to be replaced;
# the cookie needs to be reset.
request.csrf_cookie_needs_reset = True
return csrf_token
# /django/middleware/csrf.py
CSRF_SECRET_LENGTH = 32
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
def _sanitize_token(token):
# Allow only ASCII alphanumerics
# 仅允许ASCII字母数字
if re.search('[^a-zA-Z0-9]', token):
return _get_new_csrf_token()
先跳转到_get_new_csrf_token()
,看他的生成方法
def _get_new_csrf_token():
return _salt_cipher_secret(_get_new_csrf_string())
CSRF_SECRET_LENGTH = 32
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
def _get_new_csrf_string():
return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)
def _salt_cipher_secret(secret):
"""
Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
token by adding a salt and using it to encrypt the secret.
给定一个secret(假设是一串CSRF_ALLOWED_CHARS),通过添加一个随机生成值并使用它来加
密secret来生成一个token。
"""
salt = _get_new_csrf_string()
chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
return salt + cipher
# django/utils/crypto.py
def get_random_string(length=12,
allowed_chars='abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
"""
Return a securely generated random string.
返回安全生成的随机字符串。
The default length of 12 with the a-z, A-Z, 0-9 character set returns
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
"""
if not using_sysrandom:
# This is ugly, and a hack, but it makes things better than
# the alternative of predictability. This re-seeds the PRNG
# using a value that is hard for an attacker to predict, every
# time a random string is required. This may change the
# properties of the chosen random sequence slightly, but this
# is better than absolute predictability.
random.seed(
hashlib.sha256(
('%s%s%s' % (random.getstate(), time.time(), settings.SECRET_KEY)).encode()
).digest()
)
return ''.join(random.choice(allowed_chars) for i in range(length))
返回的是一个随机的字符串
# 接上面 def _sanitize_token
elif len(token) == CSRF_TOKEN_LENGTH:
return token
elif len(token) == CSRF_SECRET_LENGTH:
# Older Django versions set cookies to values of CSRF_SECRET_LENGTH
# alphanumeric characters. For backwards compatibility, accept
# such values as unsalted secrets.
# It's easier to salt here and be consistent later, rather than add
# different code paths in the checks, although that might be a tad more
# efficient.
# 较旧的Django版本将cookie设置为CSRF_SECRET_LENGTH字母数字字符的值。 为了向后
# 兼容,接受诸如无保密秘密之类的值。这里更容易加盐并在以后保持一致,而不是在检查
# 中添加不同的代码路径,尽管这可能会更有效。
return _salt_cipher_secret(token)
return _get_new_csrf_token()
CsrfViewMiddleware.process_view
# django/middleware/csrf.py
class CsrfViewMiddleware(MiddlewareMixin):
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works
# 如果装饰器 @csrf_exempt 生效,则不处理
if getattr(callback, 'csrf_exempt', False):
return None
# Assume that anything not defined as 'safe' by RFC7231 needs protection
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
# Mechanism to turn off CSRF checks for test suite.
# It comes after the creation of CSRF cookies, so that
# everything else continues to work exactly the same
# (e.g. cookies are sent, etc.), but before any
# branches that call reject().
# 关闭CSRF检查测试套件的机制。在创建CSRF cookie之后,所以
# 其他所有内容继续完全相同(例如发送cookie等),但在调用
# reject()的任何分支之前。
return self._accept(request)
def _accept(self, request):
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None
接上面CsrfViewMiddleware.process_view
的代码
# is_secure 如果请求是安全的,返回True,意味着发出的是HTTPS请求。
if request.is_secure():
referer = request.META.get('HTTP_REFERER')
if referer is None:
return self._reject(request, REASON_NO_REFERER)
# _reject就是csrf验证不通过,因为reffer为空
返回一个丑拒的代码
def _reject(self, request, reason):
logger.warning(
'Forbidden (%s): %s', reason, request.path,
extra={
'status_code': 403,
'request': request,
}
)
return _get_failure_view()(request, reason=reason)
referer = urlparse(referer)
# referer.scheme: 请求的协议,一般为http或者https
# referer.netloc: host域名
# 确保我们有一个有效的url在Referer中.
if '' in (referer.scheme, referer.netloc):
return self._reject(request, REASON_MALFORMED_REFERER)
# Ensure that our Referer is also secure.
if referer.scheme != 'https':
return self._reject(request, REASON_INSECURE_REFERER)
# If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
# match on host:port. If not, obey the cookie rules (or those
# for the session cookie, if CSRF_USE_SESSIONS).
good_referer = (
settings.SESSION_COOKIE_DOMAIN
if settings.CSRF_USE_SESSIONS
else settings.CSRF_COOKIE_DOMAIN
)
if good_referer is not None:
server_port = request.get_port()
if server_port not in ('443', '80'):
good_referer = '%s:%s' % (good_referer, server_port)
else:
# request.get_host() includes the port.
good_referer = request.get_host()
# 在这里,我们生成所有可接受的HTTP引用的列表,包括当前主机,因
# 为它已在上游验证。
# CSRF_TRUSTED_ORIGINS global_settings.py里为空的list,设置可
# 以信任的来源
good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
good_hosts.append(good_referer)
# 禁止跨域
if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
reason = REASON_BAD_REFERER % referer.geturl()
return self._reject(request, reason)
csrf_token = request.META.get('CSRF_COOKIE')
if csrf_token is None:
# 没有CSRF cookie。对于POST请求,我们坚持使用CSRF
# cookie,这样我们就可以避免所有CSRF攻击,包括登录CSRF。
return self._reject(request, REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
# request.POST.get() 相当于获取request.POST['csrfmiddlewaretoken']的值,
# 若果出错就返回 ''.这里的csrfmiddlewaretoken是提交的表单中的值,在
# 模板中用{% csrf_token %} 生成
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except IOError:
# Handle a broken connection before we've completed reading
# the POST data. process_view shouldn't raise any
# exceptions, so we'll ignore and serve the user a 403
# (assuming they're still listening, which they probably
# aren't because of the error).
# 在我们完成读取POST数据之前处理断开的连接。
# process_view不应该引发任何exception,因此我们将忽略并返回403
#(假设他们仍在监听,他们可能不是因为错误)。
pass
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE.
# ajax中适用'X-CSRFToken'
# CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
request_csrf_token = _sanitize_token(request_csrf_token)
# 对比两个csrf_token,一个是表单里隐藏的csrfmiddlewaretoken
#(或者ajax的hearder: X_CSRFTOKEN),另一个是自带的cookies里的csrf_token
if not _compare_salted_tokens(request_csrf_token, csrf_token):
# 匹配不对就拒绝
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
def _compare_salted_tokens(request_csrf_token, csrf_token):
# Assume both arguments are sanitized -- that is, strings of
# length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
return constant_time_compare(
_unsalt_cipher_token(request_csrf_token),
_unsalt_cipher_token(csrf_token),
)def _unsalt_cipher_token(token):
"""
Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt
the second half to produce the original secret.
"""
salt = token[:CSRF_SECRET_LENGTH]
token = token[CSRF_SECRET_LENGTH:]
chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
secret = ''.join(chars[x - y] for x, y in pairs) # Note negative values are ok
return secret
def _accept(self, request):
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None
get_token(重要)
get_token
是在外部调用,由 Template 中的{% csrf_token %}
触发,由request的cookie不同做出不同的反应。
def get_token(request):
if "CSRF_COOKIE" not in request.META:
# 如果request中不存在csrf,先生成一个新的secret,加密赋值到META["CSRF_COOKIE"] 中,
# 后面用来放到set_cookie之中
csrf_secret = _get_new_csrf_string()
request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
else:
# 如果request的cookie中存在了csrf_token,冲洗解密,取出secret csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
request.META["CSRF_COOKIE_USED"] = True
# 返回另外一个加密生成的secret, 由于加密是随机的,所以与上面的META["CSRF_COOKIE"]不一样
return _salt_cipher_secret(csrf_secret)
上面返回的一个加密的secret
将会被填充进入 <input type="hidden" name="csrfmiddlewaretoken" value="{}" >
value里面,随着表单一起提交并和cookie之中的csrf_token比较。
CsrfViewMiddleware.process_response
def process_response(self, request, response):
if not getattr(request, 'csrf_cookie_needs_reset', False):
if getattr(response, 'csrf_cookie_set', False):
return response
if not request.META.get("CSRF_COOKIE_USED", False):
return response
# Set the CSRF cookie even if it's already set, so we renew
# the expiry timer.
self._set_token(request, response)
response.csrf_cookie_set = True
return response
# 设置token
def _set_token(self, request, response):
if settings.CSRF_USE_SESSIONS:
request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
else:
response.set_cookie(
settings.CSRF_COOKIE_NAME,
# request.META['CSRF_COOKIE']就是在上面赋值的
request.META['CSRF_COOKIE'],
max_age=settings.CSRF_COOKIE_AGE,
domain=settings.CSRF_COOKIE_DOMAIN,
path=settings.CSRF_COOKIE_PATH,
secure=settings.CSRF_COOKIE_SECURE,
httponly=settings.CSRF_COOKIE_HTTPONLY,
)
# Set the Vary header since content varies with the CSRF cookie.
patch_vary_headers(response, ('Cookie',))
总结
- 第一次访问页面
- 首先第一次访问页面,Template中的
{% csrf_token %}
会启动get_token
(不是私有方法_get_token
),生产一个csrf_secret的值。 - 这个值在
_salt_cipher_secret
中随机生产一个与csrf_secret长度相同的salt,利用salt加密csrf_secret,两个字符串拼接形成csrf_token,request.META['CSRF_COOKIE'] = csrf_token
并设置到cookie里面。 get_token
返回的用随机生成的另外一个salt加密csrf_secret,同样拼接返回放入隐藏的input
之中
- 首先第一次访问页面,Template中的
- 向页面提交表单
- 提交的cookie中含有的csrf_token与表单提交的
csrfmiddlewaretoken
在process_view
进行解密,比对,如果解密出来的数值不同直接返回_reject()
- 提交的cookie中含有的csrf_token与表单提交的
django中间件CsrfViewMiddleware源码分析,探究csrf实现的更多相关文章
- Django中间件CsrfViewMiddleware源码分析
Django Documentation csrf保护基于以下: 1, 一个CSRF cookie基于一个随机生成的值,其他网站无法得到,次cookie有CsrfViewMiddleware产生.它与 ...
- Django中间件部分源码分析
中间件源码分析 中间件简介 中间件是一个用来处理Django的请求和响应的框架级别的钩子.它是一个轻量.低级别的插件系统,用于在全局范围内改变Django的输入和输出.每个中间件组件都负责做一些特定的 ...
- Django搭建及源码分析(三)---+uWSGI+nginx
每个框架或者应用都是为了解决某些问题才出现旦生的,没有一个事物是可以解决所有问题的.如果觉得某个框架或者应用使用很不方便,那么很有可能就是你没有将其使用到正确的地方,没有按开发者的设计初衷来使用它,当 ...
- Django如何启动源码分析
Django如何启动源码分析 启动 我们启动Django是通过python manage.py runsever的命令 解决 这句话就是执行manage.py文件,并在命令行发送一个runsever字 ...
- Django之DRF源码分析(二)---数据校验部分
Django之DRF源码分析(二)---数据校验部分 is_valid() 源码 def is_valid(self, raise_exception=False): assert not hasat ...
- Django rest framework源码分析(一) 认证
一.基础 最近正好有机会去写一些可视化的东西,就想着前后端分离,想使用django rest framework写一些,顺便复习一下django rest framework的知识,只是顺便哦,好吧. ...
- django 之(二) --- 源码分析
CBV类视图继承 CBV:继承自View:注册的时候使用的as_view() 入口 不能使用请求方法的名字作为参数的名字 只能接受已经存在的属性对应的参数 定义了一个view 创建了一个类视图对象 保 ...
- 开源分布式数据库中间件MyCat源码分析系列
MyCat是当下很火的开源分布式数据库中间件,特意花费了一些精力研究其实现方式与内部机制,在此针对某些较为重要的源码进行粗浅的分析,希望与感兴趣的朋友交流探讨. 本源码分析系列主要针对代码实现,配置. ...
- Django rest framework源码分析(3)----节流
目录 Django rest framework(1)----认证 Django rest framework(2)----权限 Django rest framework(3)----节流 Djan ...
随机推荐
- Sigrity PowerDC是如何计算IR Drop Margin?
IR Drop仿真是一个系统层面的问题,需要考虑完整的Power Distribution System(PDS)链路上所有压降,并以此来优化每颗器件所接收到的供电电压. 在设计设计中所有的电源供电芯 ...
- xfs管理2T以上大分区
设置gpt硬盘 parted /dev/sdb (parted) mklabel gpt (parted) mkpart primary xfs 0 -1 (Ignore) (parted) quit ...
- Windows下安装ElasticSearch及工具
转载自个人主页 前言 什么是ElasticSearch 官网如是介绍:Elasticsearch 是一个分布式.可扩展.实时的搜索与数据分析引擎. 它能从项目一开始就赋予你的数据以搜索.分析和探索的能 ...
- Visual Studio 编辑器打开项目后,一直提醒Vs在忙,解决方法
今天打开VS2015后,因为这个解决中有很项目,突然就一直现在加载中,点击VS提示在忙,怎么破那?请往下看 第一种方法 1.关闭VS: 2.去C:\Users\<your users name& ...
- 调试wmi连接主机进行监控
wmi连接及配置 1.首先介绍两款连接wmi的工具: ①windows自带的wbemtest. ②WMI 资源浏览器 两者都是可以连接wmi,并且执行相关命令的工具. 2.尝试连接首先利用win自带的 ...
- 二叉树遍历,先序序列+中序序列=后序序列,Poj(2255)
这里我参考了JHF大神的写法啦,直接把输出写在了建树的过程中了. 思路: 先根据先序序列找到根节点,在找该节点在中序序列中的位置,这样,左右子树有分开了.这里的细节值得注意一下,不然很容易建树出错.( ...
- 【转】Android tools:context
tools:context="com.example.guolin.scrollertest.MainActivity" 有时候可以看到有这个东西,但是从来没有用过,不知道有什么作 ...
- c# 语法 字符串内插
结果截屏 参考文章 https://www.cnblogs.com/csproj/p/Interpolated_Strings.html c# 6.0语法 https://www.cnblogs.co ...
- Java 文件切割工具类
Story: 发送MongoDB 管理软件到公司邮箱,工作使用. 1.由于公司邮箱限制附件大小,大文件无法发送,故做此程序用于切割大文件成多个小文件,然后逐个发送. 2.收到小文件之后,再重新组合成原 ...
- mac home brew install go
mac利器home brew安装Go 首先你得需要安装home brew和ruby环境(因为home brew依赖ruby) 如果没有请自行到链接安装 准备好之后就开始安装go了 brew updat ...