跨站请求伪造保护

CSRF 中间件和模板标签提供了针对 跨站请求伪造 (Cross Site Request Forgeries) 的简单保护机制。这种类型的攻击发生在恶意网站包含一个链接、一个表单按钮或一些 JavaScript,旨在使用已登录用户的凭据在其浏览器中访问恶意网站以执行某些操作。还有一种相关的攻击类型叫做 ‘登录 CSRF’,其中攻击者的网站会欺骗用户的浏览器,以使用其他用户的凭据登录到某个站点,这也得到了保护。

对抗 CSRF 攻击的第一道防线是确保 GET 请求(以及其他按照 RFC 9110#section-9.2.1 定义的 ‘安全’ 方法)是无副作用的。然后,可以按照 如何使用 Django 提供的 CSRF 防护功能 中概述的步骤来保护通过 ‘不安全’ 方法(如 POST、PUT 和 DELETE)的请求。

工作方式

CSRF 保护是基于以下几点:

  1. CSRF cookie 是一个随机的秘密值,其他网站无法访问。

    CsrfViewMiddleware 在每次调用 django.middleware.csrf.get_token() 时都会将这个 cookie 与响应一起发送。出于安全原因,每当用户登录时,这个秘密值的值都会更改。

  2. 一个名为 ‘csrfmiddlewaretoken’ 的隐藏表单字段,出现在所有发送的 POST 表单中。

    为了防止 BREACH 攻击,这个字段的值不仅仅是秘密值。它在每个响应中都使用一个掩码进行不同方式的混淆。每次调用 get_token() 时,都会随机生成一个掩码,因此表单字段的值每次都不同。

    这一部分是由模板标签来完成的。

  3. 对于所有不使用 HTTP GET、HEAD、OPTIONS 或 TRACE 的传入请求,必须存在一个 CSRF cookie,并且“csrfmiddlewaretoken”字段必须存在且正确。如果不存在,用户将得到一个 403 错误。

    当验证“csrfmiddlewaretoken”字段值时,只有密钥,而不是完整的令牌,会与 cookie 值中的密钥进行比较。这允许使用不断变化的令牌。虽然每个请求都可能使用自己的令牌,但密钥对所有请求都是通用的。

    这个检查是由 CsrfViewMiddleware 完成的。

  4. CsrfViewMiddleware 根据当前主机和 CSRF_TRUSTED_ORIGINS 的设置,验证 Origin header ,如果是由浏览器提供的。这提供了对跨子域攻击的保护。

  5. 此外,对于 HTTPS 请求,如果没有提供 Origin 头,CsrfViewMiddleware 会执行严格的来源检查。这意味着,即使一个子域可以设置或修改你的域名上的 cookie,它也不能强迫用户向你的应用程序发布,因为该请求不会来自你自己的确切域名。

    这也解决了在 HTTPS 下使用独立于会话的密钥时可能出现的中间人攻击问题,这是因为 HTTP Set-Cookie 头会被客户接受(不幸的是),即使他们在 HTTPS 下与一个网站对话。对 HTTP 请求不进行 Referer 检查,因为 HTTP 下 Referer 头的存在不够可靠)。

    如果设置了 CSRF_COOKIE_DOMAIN 设置,则会将 referer 与之进行比较。你可以通过包含一个前导点号来允许跨子域请求。例如,CSRF_COOKIE_DOMAIN = '.example.com' 将允许来自 www.example.comapi.example.com 的 POST 请求。如果没有设置,那么 referer 必须与 HTTP Host 头匹配。

    通过 CSRF_TRUSTED_ORIGINS 设置,可以将接受的 referer 扩展到当前主机或 cookie 域之外。

Changed in Django 4.1:

在较旧的版本中,CSRF cookie 的值是经过掩码处理的。

这确保了只有源自受信任域的表单才能用于 POST 回数据。

它故意忽略 GET 请求(以及其他被 RFC 9110#section-9.2.1 定义为 ‘安全’ 的请求)。这些请求不应该具有潜在的危险副作用,因此通过 GET 请求进行的 CSRF 攻击应该是无害的。RFC 9110#section-9.2.1 将 POST、PUT 和 DELETE 定义为 ‘不安全’,而所有其他方法也被假定为不安全,以提供最大的保护。

CSRF 保护不能防止中间人攻击,所以使用 HTTPSHTTP 严格传输安全。它还假设 验证 HOST 头 和你的网站上没有任何 跨站脚本漏洞 (因为 XSS 漏洞已经让攻击者做了 CSRF 漏洞允许的任何事情,甚至更糟)。

删除 Referer

为了避免向第三方网站透露 referrer URL,你可能想在你的网站的 <a> 标签上 禁用 referrer 。例如,你可以使用 <meta name="referrer" content="no-referrer"> 标签或包含 Referrer-Policy: no-referrer 头。由于 CSRF 保护对 HTTPS 请求进行严格的 referer 检查,这些技术会在使用“不安全”方法的请求上导致 CSRF 失败。取而代之的是,使用诸如 <a rel="noreferrer" ...>" 这样的替代品来链接第三方网站。

限制

站点内的子域名将能够在整个域上为客户端设置 cookie。通过设置 cookie 并使用相应的令牌,子域名将能够绕过 CSRF 保护。避免这种情况的唯一方法是确保子域名由可信任的用户控制(或者至少不能设置 cookie)。请注意,即使没有 CSRF,还存在其他漏洞,例如会话固定,这些漏洞会使将子域名交给不受信任的方可能不是一个好主意,而且这些漏洞在当前的浏览器中不能轻易修复。

实用程序

下面的例子假设你使用的是基于函数的视图。如果你正在使用基于类的视图,你可以参考 装饰基于类的视图

csrf_exempt(view)

该装饰器标记着一个视图被免除了中间件所确保的保护。例如:

  1. from django.http import HttpResponse
  2. from django.views.decorators.csrf import csrf_exempt
  3. @csrf_exempt
  4. def my_view(request):
  5. return HttpResponse("Hello world")

csrf_protect(view)

为视图提供 CsrfViewMiddleware 保护的装饰器。

用法:

  1. from django.shortcuts import render
  2. from django.views.decorators.csrf import csrf_protect
  3. @csrf_protect
  4. def my_view(request):
  5. c = {}
  6. # ...
  7. return render(request, "a_template.html", c)

requires_csrf_token(view)

通常情况下,如果 CsrfViewMiddleware.process_view 或类似 csrf_protect 这样的等价物没有运行, csrf_token 模板标签将无法工作。视图装饰器 requires_csrf_token 可以用来确保模板标签工作。这个装饰器的工作原理与 csrf_protect 类似,但绝不会拒绝接收到的请求。

举例:

  1. from django.shortcuts import render
  2. from django.views.decorators.csrf import requires_csrf_token
  3. @requires_csrf_token
  4. def my_view(request):
  5. c = {}
  6. # ...
  7. return render(request, "a_template.html", c)

ensure_csrf_cookie(view)

该装饰器强制视图发送 CSRF cookie。

配置

一些配置可以用来控制Django 的 CSRF 行为:

常问问题

可以提交任意的 CSRF 令牌对(cookie 和 POST 数据)是漏洞吗?

不,这是设计好的。如果没有中间人攻击,攻击者就没有办法向受害者的浏览器发送 CSRF 令牌 cookie,所以成功的攻击需要通过 XSS 或类似的方式获得受害者浏览器的 cookie,在这种情况下,攻击者通常不需要 CSRF 攻击。

一些安全审计工具将此标记为问题,但如前所述,攻击者无法窃取用户浏览器的 CSRF cookie。使用 Firebug、Chrome 开发工具等“窃取”或修改 自己的 令牌并不是漏洞。

Django 的 CSRF 保护默认不与会话关联,是不是有问题?

不,这是设计好的。不将 CSRF 保护与会话联系起来,就可以在诸如 pastebin 这样允许匿名用户提交的网站上使用保护,而这些用户并没有会话。

如果你希望在用户的会话中存储 CSRF 令牌,请使用 CSRF_USE_SESSIONS 设置。

为什么用户登录后会遇到 CSRF 验证失败?

出于安全考虑,每次用户登录时,CSRF 令牌都会轮换。任何在登录前生成表单的页面都会有一个旧的、无效的 CSRF 令牌,需要重新加载。如果用户在登录后使用后退按钮或在不同的浏览器标签页中登录,可能会发生这种情况。