Django CSRF防御具体流程

Django对CSRF的防御主要django.middleware.csrf.CsrfViewMiddleware中间件来实现。通过在settings.pyMIDDLEWARE_CLASSES(新版的Django该项改为MIDDLEWARE)加上django.middleware.csrf.CsrfViewMiddleware来启用这个中间件。通过Django源码的django/middleware/csrf.py文件,调试程序来分析了Django CSRF防御流程。这篇文章记下我的理解。

版本说明:

Django Version:1.9.5
Python Version:2.7

Django CSRF防御具体流程

首先要清楚Django是怎么验证一个请求不是CSRF:Django会从请求头cookie取csrftoken这一项的值,再从POST表单里取csrfmiddlewaretoken这一项(或从请求头X-CSRFToken取)的值,通过比对两者是否一致来判断这个请求是不是非法,非法就返回403状态码。这里的“是否一致”,并不是判断两者的值是否相等,而是判断_unsalt_cipher_token(csrfmiddlewaretoken)_unsalt_cipher_token(csrftoken)是否相等,具体留在后面说。

而生成csrftokencsrfmiddlewaretoken的代码在get_token(request)函数

1
2
3
4
5
6
7
8
def get_token(request):
if "CSRF_COOKIE" not in request.META:
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)

这个函数会在渲染模板的时候调用,具体来说是由csrf context processor调用。
如果request.META["CRSF_COOKIE"]不存在,就调用_get_new_csrf_string()函数来生成一串随机字符(32个字符,大小写字母和数字),赋给csrf_secret,再调用_salt_cipher_secret(scrf_secret)生成64个字符的字符串赋给request.META["CSRF_COOKIE"],而这个request.META["CSRF_COOKIE"]之后用来设置COOKIE 的csrftoken
最后的返回值_salt_cipher_secret(csrf_secret)就渲染到POST表单的csrfmiddlewaretoken。值得一提的是_salt_cipher_secret(csrf_secret)每次的返回值都不一样,而csrf_secret == _unsalt_cipher_token(_salt_cipher_secret(csrf_secret))

总的来说,涉及到三个值,csrftokencsrfmiddlewaretokencsrf_secret,还有两个函数,_unsalt_cipher_token(token)_salt_cipher_secret(token)。用图来说明下这两个过程:

  1. 生成csrftokencsrfmiddlewaretoken
  2. 验证csrftokencsrfmiddlewaretoken是否一致。

生成csrftoken和csrfmiddlewaretoken.png

_unsalt_cipher_token_salt_cipher_secret这两个函数具体怎么实现呢?怎么做到_salt_cipher_secret(csrf_secret)每次返回的token值不同,调用_unsalt_cipher_token(token)就返回原来的csrf_secret

用图来表示(简化下,把csrf_secret长度改为3)

Django CSRF TOKEN具体生成和验证算法.png

上面的过程主要涉及到的数值运算就这两条式子(下面的符号都表示一个整数):

1
2
1. Cipher = (Secret + Salt) mod N
2. (Cipher - Salt) mod N 会等于 Secret

其它

  1. django/middleware/csrf.py文件里有个函数:rotate_token(request),这个函数用来改变csrftoken这个COOKIE。 在用户登录后(是指 django.contrib.auth这个组件的登录)调用,主要从安全考虑,避免这个COOKIE跟登录前的一样。 如果自己实现的登录逻辑,可以调用这个函数提高点安全性。
  2. 一般csrftoken这个COOKIE是不会变的,除了第一点说的登录,和不存在时重新生成一个。有时候会出现登录后csrftoken失效的情况。官网一个FAQ
  3. 为什么Django要把csrftokencsrfmiddlewaretoken设置成不相等,直接生成的时候让它们相等,验证的时候判断是否相等不就好了?个人觉得这样做有个好处,有时候csrftoken这个COOKIE前端不需要获取,可以设置成HTTP ONLY,提高点安全性。 大家怎么看?
  4. 从上面分析的算法来看,csrftokencsrfmiddlewaretoken相同也可以通过CSRF验证。所以在AJAX请求中,直接取csrftoken值加到请求中就好了。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!