为什么 Django/Django REST Framework 不深入验证 CSRF 令牌,即使使用 enforce-CSRF?

Why does Django/Django REST Framework not validate CSRF tokens in-depth, even with enforce-CSRF?

我正在尝试为对匿名用户开放的 Django Rest API 实施 CSRF。 就此而言,我尝试了两种不同的方法:

  1. 从一个 CSRFAPIView 基础视图扩展选定的 API 视图,该视图在调度方法上有一个 @ensure_csrf_cookie 注释。
  2. 使用基于 SessionAuthentication 的自定义身份验证 class,无论用户是否登录,它都适用 enforce_csrf()

在这两种方法中,CSRF 检查似乎都在表面上起作用。如果 cookie 中缺少 CSRF 令牌或令牌的长度不正确,端点 returns a 403 - Forbidden。 但是,如果我在 cookie 中编辑 CSRF 令牌的值,则可以毫无问题地接受请求。所以我可以为 CSRF 使用一个随机值,只要它的长度正确即可。

此行为似乎偏离了常规的 Django 登录视图,其中 CSRF 的内容很重要。我正在使用 debug/test_environment 标志在本地设置中进行测试。

我在 DRF 中的自定义 CSRF 检查未得到深入验证的原因可能是什么?

自定义认证代码片段:

class RestCsrfAuthentication(SessionAuthentication):
    def authenticate(self, request):
        self.enforce_csrf(request)
        rotate_token(request)
        return None

并在设置中:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'csrfexample.authentication.RestCsrfAuthentication',
    ]
}

实际上,Django 中 CSRF 令牌的具体内容并不重要。

This reply by a Django security team member to a question similar to yours 是这样说的:

The way our CSRF tokens work is pretty simple. Each form contains a CSRF token, which matches the CSRF cookie. Before we process the protected form, we make sure that the submitted token matches the cookie. This is a server-side check, but it's not validating against a stored server-side value. Since a remote attacker should not be able to read or set arbitrary cookies on your domain, this protects you.

Since we're just matching the cookie with the posted token, the data is not sensitive (in fact it's completely arbitrary - a cookie of "zzzz" works just fine), and so the rotation/expiration recommendations don't make any difference. If an attacker can read or set arbitrary cookies on your domain, all forms of cookie-based CSRF protection are broken, full stop.

(实际上,由于长度要求,“zzzz”将不起作用,但稍后会详细介绍。)我建议阅读整个邮件列表消息以获得更全面的理解。那里解释了 Django 在框架中的独特之处,因为 CSRF 保护独立于会话。

我通过 this FAQ item on the Django docs 找到邮件列表消息:

Is posting an arbitrary CSRF token pair (cookie and POST data) a vulnerability?

No, this is by design. Without a man-in-the-middle attack, there is no way for an attacker to send a CSRF token cookie to a victim’s browser, so a successful attack would need to obtain the victim’s browser’s cookie via XSS or similar, in which case an attacker usually doesn’t need CSRF attacks.

Some security audit tools flag this as a problem but as mentioned before, an attacker cannot steal a user’s browser’s CSRF cookie. “Stealing” or modifying your own token using Firebug, Chrome dev tools, etc. isn’t a vulnerability.

(强调我的。)

该消息来自 2011 年,但它仍然有效,为了证明这一点,让我们看一下代码。 Django REST Framework 的 SessionAuthenticationensure_csrf_cookie 装饰器都在 POSTed/X-CSRFToken 值上使用核心 Django 的 CsrfViewMiddleware (source). In that middleware class's process_view() method, you'll see that it fetches the CSRF cookie (a cookie named csrftoken by default), and then the posted CSRF token (part of the POSTed data, with a fallback to reading the X-CSRFToken header). After that, it runs _sanitize_token()。此清理步骤是检查令牌长度是否正确的地方;这就是为什么当您提供更短或更长的令牌时,您会按预期收到 403。

之后,该方法继续使用 the function _compare_salted_tokens() 比较两个值。如果您阅读该函数以及它进行的所有进一步调用,您会发现它归结为检查两个字符串是否匹配,基本上不考虑字符串的值 .

This behaviour seems to deviate from the regular Django login view, in which the contents of the CSRF do matter.

不,即使在 built-in 登录视图中也没有关系。我 运行 这个 curl 命令(Windows cmd 格式)针对大多数默认的 Django 项目:

curl -v
  -H "Cookie: csrftoken=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
  -H "X-CSRFToken: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
  -F "username=admin" -F "password=1234" http://localhost:8000/admin/login/

并且 Django 返回了一个会话 cookie(当然还有一个 CSRF cookie)。


请注意您覆盖的方式 SessionAuthentication.authenticate():您可能已经知道这一点,但是 according to the DRF docs that method should return a (User, auth) tuple instead of None if the request has session data, i.e. if the request is from a logged-in user. Also, I think rotate_token() is unnecessary, because this code only checks for authentication status, and is not concerned with actually authenticating users. (The Django sourcerotate_token()“应该在登录时完成”。)