Django: Security: CSRF

 20th August 2020 at 2:19pm

Django 如何 防范 CSRF

实现原理

实现逻辑在 django.middleware.csrf.CsrfViewMiddleware 中。

默认实现:

  1. 用户第一次向 Django 服务发起请求后,收到的回包中会带有一个 csrftoken cookie。这个 cookie 的值是根据服务端 secret 加盐生成的,如:
    Set-Cookie: csrftoken=lE3I...; expires=Tue, 27 Apr 2021 00:06:45 GMT; Max-Age=31449600; Path=/; SameSite=Lax

    对于后续的请求,这个 cookie 的值不会变,有效期会刷新。用户如果从未登录状态变为登录状态,为了安全考虑,Django 会调用 rotate_token 更新这个 cookie 值

  2. 对于使用 POST 的 <form> 表单,必须在 template 中加入 {% csrf_token %} 语句,它会自动生成一个 hidden input 嵌入表单中:
    <input type="hidden" name="csrfmiddlewaretoken" value="0c90d...">

    这个 token 的值与 cookie 中的不一样,是同个 secret 加不同的盐生成的,并且盐作为字符串的前一半附在 token 中,使得后面服务端可以 unsalt

  3. 用户点击提交时,FORM 表单及 cookie 中的 CSRF token 都被带上来。它们在后台被去盐(unsalt)后比对,如果一样,则表示请求安全;不一样时,返回 HTTP 403 Forbidden。在 HTTPS 下时,Referer 头也会被校验

对于密码学部分我未深究,在这方面没有积累。

上面描述的是传统的表单式提交,但是现在 SPA 盛行,也可能有通过 JS 发出请求的情况。对于这种情况,将 CSRF token 在 POST 体中带上在实现上往往比较啰嗦,而且接口也不一定都用 application/x-www-form-urlencoded。因此 Django 也可以接受你将 CSRF token 放在 X-CSRFToken 头上,这种情况下你可以用 JS 读 cookie 中的 token 而不是 <input> 中的 token。同时注意,如果页面的域名与 API 域名不同,也要设置好 CORS 规则。

使用

除了上文提到的主要流程,Django 还提供了一些 decorator 来控制具体的 view 要不要经过 CSRF 的校验。

设计考量

这里面有几个设计上的考虑,Django 的文档没有表达,我写下自己的理解。

在通过 JS 发异步登陆请求的场景下,Django 比对的是请求中的 X-CSRFToken 头及 token cookie;但前者实际上也来源于后者,这样做是否安全?

是安全的。攻击者除非使用 XSS 将恶意代码注入到你的页面中,不然它没有办法使用 JS 去读取到 cookie 中的 token,也就无法构造恶意请求。

但是 token 的 cookie 仍应限制好域名范围。之前大公司中,有一些因为子域名安全防范不严引起的攻击。比如 qq.com 下发了域名范围为 .qq.com(即针对全部子域名生效)的身份 cookie,同时没有设置 HttpOnly。假如此时 f.qq.com 的安全措施做得不好,被黑客 XSS 攻击了,那黑客可以在 f.qq.com 中获得用户全站的身份 cookie。黑客往往喜欢找大公司中不再有团队维护的页面做攻击。

固定的、非表单中的 CSRF token,为啥要通过 cookie 下发?

应该是为了提供 JS 发异步请求的支持。对于 SPA 场景,使用的是 React 等框架,并不使用 Django template,没法把 CSRF token 带下来;那 JS 只能去读 cookie 中的 token。

Django 为了平衡便利与安全(比如开发阶段的 debug server 往往没有上 HTTPS),这个 cookie 并没有设置上 HTTP only 及 secure。

如果你没有使用 JS 发请求的需要,也可以修改 Django 配置:

  • 将 cookie 设置为 HTTP only,减少被 XSS 时的影响(但 XSS 是无论如何都应该防下来的)
  • 使用服务端的 session 来保存,不再下发给客户端

如果你的生产环境上 HTTPS,可以区分不同环境给 cookie 设置上 secure flag。

Django 将 CSRF token 通过 cookie 下发,是否增加了被 CSRF 的可能性?

对于现代浏览器,它们能理解 Django 在 cookie 的 SameSite 上配置了 Lax,这可以做到很好的防护。但即使是未支持 SameSite 的浏览器,Form 中的 csrf token 也可以防住攻击。

但是假如你的站点被 XSS 攻击,那这些防范措施都没有作用,黑客可以以用户身份做任何事情。这个时候他也不需要费劲去搞 CSRF。

Token 为什么要加盐?

如果不加盐,每个 CSRF token 都是一样的,失去了保护意义。

同时它可以防暴力爆破。Django 是开源软件,token 的生成过程是公开的,如果不加盐,黑客可以预先生成一个 secret 对应生成后的 token 的彩虹表,然后通过 token 反查出 secret,于是服务端安全不复存在。

为什么 Django 默认不将 CSRF 保护与具体的会话关联在一起?

文档里有 提及。是为了支持匿名会话,即用户不登录也可以使用。所以如果你用默认的 cookie 实现而不是用 session,那 session 的 middleware 不启用也没有关系。