Django中间件CsrfViewMiddleware源码分析
Posted chenrun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Django中间件CsrfViewMiddleware源码分析相关的知识,希望对你有一定的参考价值。
Django Documentation
csrf保护基于以下:
1, 一个CSRF cookie基于一个随机生成的值,其他网站无法得到,次cookie有CsrfViewMiddleware产生.它与每个调用django.middleware.csrf.get_token()(这是一个用于取回CSRF token的方法)的响应一起发送,如果它尚未在请求上设置的话.
为了放置BREACH攻击,token不仅仅是比吗,随机的salt被置于secret之前并用来加密它,出于安全原因,每次用户登陆都会更改密钥的值.
CsrfViewMiddleware.process_request
class CsrfViewMiddleware(MiddlewareMixin):
def process_request(self, request):
csrf_token = self._get_token(request)
# 第一次访问, csrf_token返回None,
if csrf_token is not None:
request.META["CSRF_COOKIE"] = csrf_token
# request.META是一个python字典,包含了所有本次Http请求的Header信息,比如用户IP地址和用户Agent(通常是浏览器的名称版本号).
settings = Lazysettins()
这是一个懒加载
方法_get_token,从名字上看就是获取token, _get_token在后面多处地方用到
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
CSRF_SECRET_LENGTH = 32 CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH def _sanitize_token(token): # Allow only ASCII alphanumerics if re.search(‘[^a-zA-Z0-9]‘, force_text(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‘): """ Returns 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(‘utf-8‘) ).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()
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为空
返回一个Forbidden的代码
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域名 # Make sure we have a valid URL for Referer. # 确保我们在referer中有一个有效的URL 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() # Here we generate a list of all acceptable HTTP referers, # including the current host since that has been validated # upstream. # 在这里,我们生成所有可能接受HTTP引用的列表,包括当前主机,因为 # 它已经在上游验证. 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: # No CSRF cookie. For POST requests, we insist on a CSRF cookie, # and in this way we can avoid all CSRF attacks, including login # 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" # CERF_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的header: 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(important)
get_token是在外部调用,由Template中的{% csrf_token %}触发,由request的cookie不同做出不同的反应.
def get_token(request): """ Returns the CSRF token required for a POST form. The token is an alphanumeric value. A new token is created if one is not already set. A side effect of calling this function is to make the csrf_protect decorator and the CsrfViewMiddleware add a CSRF cookie and a ‘Vary: Cookie‘ header to the outgoing response. For this reason, you may need to use this function lazily, as is done by the csrf context processor. """ 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: csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"]) request.META["CSRF_COOKIE_USED"] = True 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(不是私有方法), 产生一个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,同样拼接返回放入process_view进行解密,比对,如果解密出来的数值不同直接返回_reject()
-
所有传出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域之外。
以上是关于Django中间件CsrfViewMiddleware源码分析的主要内容,如果未能解决你的问题,请参考以下文章