跨站请求伪造保护 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/ref/csrf
跳转至:导航、​搜索

跨站请求伪造保护

CSRF 中间件和模板标签提供易于使用的保护,防止 跨站点请求伪造 。 当恶意网站包含链接、表单按钮或一些 JavaScript,旨在使用在其浏览器中访问恶意网站的登录用户的凭据对您的网站执行某些操作时,就会发生此类攻击。 还涵盖了一种相关类型的攻击,即“登录 CSRF”,其中攻击站点诱使用户的浏览器使用其他人的凭据登录站点。

抵御 CSRF 攻击的第一道防线是确保 GET 请求(以及其他“安全”方法,如 RFC 7231#section-4.2.1 所定义)没有副作用。 然后可以按照以下步骤保护通过“不安全”方法(例如 POST、PUT 和 DELETE)发出的请求。

如何使用它

要在您的视图中利用 CSRF 保护,请执行以下步骤:

  1. CSRF 中间件默认在 :setting:`MIDDLEWARE` 设置中激活。 如果您覆盖该设置,请记住 'django.middleware.csrf.CsrfViewMiddleware' 应该出现在任何假设已处理 CSRF 攻击的视图中间件之前。

    如果您禁用了它(不推荐),您可以在要保护的特定视图上使用 csrf_protect()(见下文)。

  2. 在任何使用 POST 表单的模板中,如果表单用于内部 URL,请在 <form> 元素内使用 :ttag:`csrf_token` 标签,例如:

    <form method="post">{% csrf_token %}

    对于针对外部 URL 的 POST 表单不应该这样做,因为这会导致 CSRF 令牌泄漏,从而导致漏洞。

  3. 在对应的视图函数中,确保使用 RequestContext 来渲染响应,这样 {% csrf_token %} 才能正常工作。 如果你正在使用 render() 函数、通用视图或 contrib 应用程序,你已经被覆盖了,因为这些都使用 RequestContext

AJAX

虽然上述方法可用于 AJAX POST 请求,但它有一些不便:您必须记住将 CSRF 令牌作为 POST 数据与每个 POST 请求一起传递。 为此,有一种替代方法:在每个 XMLHttpRequest 上,将自定义 X-CSRFToken 标头(由 :setting:`CSRF_HEADER_NAME` 设置指定)设置为 CSRF 令牌的值. 这通常更容易,因为许多 JavaScript 框架提供了允许在每个请求上设置标头的钩子。

首先,您必须获得 CSRF 令牌。 如何做到这一点取决于是否启用了 :setting:`CSRF_USE_SESSIONS`:setting:`CSRF_COOKIE_HTTPONLY` 设置。

在 AJAX 请求上设置令牌

最后,您需要在 AJAX 请求上设置标头。 使用 fetch() API:

const request = new Request(
    /* URL */,
    {headers: {'X-CSRFToken': csrftoken}}
);
fetch(request, {
    method: 'POST',
    mode: 'same-origin'  // Do not send CSRF token to another domain.
}).then(function(response) {
    // ...
});

在 Jinja2 模板中使用 CSRF

Django 的 Jinja2 模板后端将 模板:Csrf input 添加到所有模板的上下文中,相当于 Django 模板语言中的 {% csrf_token %}。 例如:

<form method="post">{{ csrf_input }}

装饰器方法

您可以在需要保护的特定视图上使用具有完全相同功能的 csrf_protect 装饰器,而不是添加 CsrfViewMiddleware 作为全面保护。 它必须用于 both 在输出中插入 CSRF 令牌的视图,以及接受 POST 表单数据的视图。 (这些通常是相同的视图函数,但并非总是如此)。

不建议单独使用装饰器,因为如果你忘记使用它,你就会有一个安全漏洞。 使用两者的“腰带和括号”策略很好,并且会产生最小的开销。

csrf_protect(view)

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

用法:

from django.shortcuts import render
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def my_view(request):
    c = {}
    # ...
    return render(request, "a_template.html", c)

如果使用基于类的视图,可以参考装饰基于类的视图


被拒绝的请求

默认情况下,如果传入请求未通过 CsrfViewMiddleware 执行的检查,则会向用户发送“403 Forbidden”响应。 这通常只有在存在真正的跨站点请求伪造时才能看到,或者由于编程错误,CSRF 令牌未包含在 POST 表单中。

但是,错误页面不是很友好,因此您可能需要提供自己的视图来处理这种情况。 为此,请设置 :setting:`CSRF_FAILURE_VIEW` 设置。

CSRF 失败作为警告记录到 django.security.csrf 记录器。


工作原理

CSRF 保护基于以下几点:

  1. 基于随机秘密值的 CSRF cookie,其他站点无法访问。

    该cookie由CsrfViewMiddleware设置。 如果尚未在请求中设置,它会与调用 django.middleware.csrf.get_token()(内部用于检索 CSRF 令牌的函数)的每个响应一起发送。

    为了防止BREACH攻击,令牌不仅仅是秘密; 一个随机掩码被预先添加到秘密并用于对其进行加扰。

    出于安全原因,每次用户登录时,密钥的值都会更改。

  2. 所有外发 POST 表单中都存在名称为“csrfmiddlewaretoken”的隐藏表单字段。 该字段的值再次是秘密的值,带有一个掩码,该掩码被添加到它并用于对其进行加扰。 每次调用 get_token() 时都会重新生成掩码,以便在每个此类响应中更改表单字段值。

    这部分由模板标签完成。

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

    验证 'csrfmiddlewaretoken' 字段值时,仅将机密而不是完整令牌与 cookie 值中的机密进行比较。 这允许使用不断变化的令牌。 虽然每个请求都可以使用自己的令牌,但秘密对所有人来说仍然是共同的。

    该检查由 CsrfViewMiddleware 完成。

  4. 另外,对于HTTPS请求,CsrfViewMiddleware做了严格的referer检查。 这意味着即使子域可以在您的域上设置或修改 cookie,它也不能强制用户发布到您的应用程序,因为该请求不会来自您自己的确切域。

    这也解决了使用会话无关秘密时在 HTTPS 下可能发生的中间人攻击,因为 HTTP Set-Cookie 标头(不幸的是)被客户端接受,即使他们正在与HTTPS 下的站点。 (不会对 HTTP 请求进行引用检查,因为 Referer 标头的存在在 HTTP 下不够可靠。)

    如果设置了 :setting:`CSRF_COOKIE_DOMAIN` 设置,则将引用者与其进行比较。 您可以通过包含前导点来允许跨子域请求。 例如,CSRF_COOKIE_DOMAIN = '.example.com' 将允许来自 www.example.comapi.example.com 的 POST 请求。 如果未设置该设置,则引用者必须匹配 HTTP Host 标头。

    可以使用 :setting:`CSRF_TRUSTED_ORIGINS` 设置将接受的引用扩展到当前主机或 cookie 域之外。

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

它故意忽略 GET 请求(以及其他被 RFC 7231#section-4.2.1 定义为“安全”的请求)。 这些请求不应该有任何潜在危险的副作用,因此带有 GET 请求的 CSRF 攻击应该是无害的。 RFC 7231#section-4.2.1 将 POST、PUT 和 DELETE 定义为“不安全”,并且所有其他方法也被假定为不安全,以实现最大程度的保护。

CSRF 保护无法防止中间人攻击,因此使用 HTTPSHTTP 严格传输安全 。 它还假设 HOST 标头 验证并且您的站点上没有任何 跨站点脚本漏洞 (因为 XSS 漏洞已经让攻击者可以执行 CSRF 漏洞允许的任何操作,并且更糟糕)。

卸下 Referer 接头

为避免将引荐来源网址泄露给第三方网站,您可能需要 在您网站的 <a> 标签上禁用引荐来源网址 。 例如,您可以使用 <meta name="referrer" content="no-referrer"> 标签或包含 Referrer-Policy: no-referrer 标头。 由于 CSRF 保护对 HTTPS 请求进行严格的引用检查,这些技术会导致使用“不安全”方法的请求出现 CSRF 失败。 相反,使用 <a rel="noreferrer" ...>" 等替代方法链接到第三方网站。


缓存

如果 :ttag:`csrf_token` 模板标签被模板使用(或 get_token 函数以其他方式调用),CsrfViewMiddleware 将添加一个 cookie 和一个Vary: Cookie 响应头。 这意味着,如果按照说明使用中间件,它会与缓存中间件很好地配合使用(UpdateCacheMiddleware 在所有其他中间件之前)。

但是,如果您在单个视图上使用缓存装饰器,CSRF 中间件将无法设置 Vary 标头或 CSRF cookie,并且响应将在没有任何一个的情况下被缓存。 在这种情况下,在任何需要插入 CSRF 令牌的视图上,您应该首先使用 django.views.decorators.csrf.csrf_protect() 装饰器:

from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_protect

@cache_page(60 * 15)
@csrf_protect
def my_view(request):
    ...

如果使用基于类的视图,可以参考装饰基于类的视图


测试

CsrfViewMiddleware 通常会成为测试视图函数的一大障碍,因为每个 POST 请求都需要发送 CSRF 令牌。 出于这个原因,Django 的 HTTP 测试客户端已被修改为在请求上设置一个标志,这放松了中间件和 csrf_protect 装饰器,以便它们不再拒绝请求。 在所有其他方面(例如 发送 cookie 等),它们的行为相同。

如果由于某种原因,您 希望 测试客户端执行 CSRF 检查,您可以创建一个执行 CSRF 检查的测试客户端实例:

>>> from django.test import Client
>>> csrf_client = Client(enforce_csrf_checks=True)

限制

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


边缘情况

某些视图可能有不寻常的要求,这意味着它们不符合此处设想的正常模式。 在这些情况下,许多实用程序可能很有用。 以下部分描述了可能需要它们的场景。

公用事业

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

csrf_exempt(view)

此装饰器将视图标记为不受中间件确保的保护。 例子:

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_view(request):
    return HttpResponse('Hello world')
requires_csrf_token(view)

通常,如果 CsrfViewMiddleware.process_view 或类似 csrf_protect 没有运行,:ttag:`csrf_token` 模板标签将不起作用。 视图装饰器 requires_csrf_token 可用于确保模板标签起作用。 这个装饰器的工作方式与 csrf_protect 类似,但从不拒绝传入的请求。

例子:

from django.shortcuts import render
from django.views.decorators.csrf import requires_csrf_token

@requires_csrf_token
def my_view(request):
    c = {}
    # ...
    return render(request, "a_template.html", c)
ensure_csrf_cookie(view)
这个装饰器强制一个视图发送 CSRF cookie。


场景

应仅针对少数视图禁用 CSRF 保护

大多数视图需要 CSRF 保护,但少数不需要。

解决方案:与其禁用中间件并将 csrf_protect 应用于所有需要它的视图,不如启用中间件并使用 csrf_exempt()


未使用 CsrfViewMiddleware.process_view

在某些情况下,CsrfViewMiddleware.process_view 在您的视图运行之前可能没有运行 - 例如 404 和 500 处理程序 - 但您仍然需要表单中的 CSRF 令牌。

解决方案:使用requires_csrf_token()


未受保护的视图需要 CSRF 令牌

可能有一些视图不受保护,已经被csrf_exempt豁免,但仍然需要包含CSRF令牌。

解决方案:使用 csrf_exempt() 后跟 requires_csrf_token()。 (IE requires_csrf_token 应该是最里面的装饰器)。


视图需要对一条路径进行保护

视图仅在一组条件下需要 CSRF 保护,并且在其余时间都不需要它。

解决方案:对整个视图函数使用 csrf_exempt(),对其中需要保护的路径使用 csrf_protect()。 例子:

from django.views.decorators.csrf import csrf_exempt, csrf_protect

@csrf_exempt
def my_view(request):

    @csrf_protect
    def protected_path(request):
        do_something()

    if some_condition():
       return protected_path(request)
    else:
       do_something_else()

页面使用 AJAX,没有任何 HTML 表单

页面通过 AJAX 发出 POST 请求,并且页面没有带有 :ttag:`csrf_token` 的 HTML 表单,这会导致发送所需的 CSRF cookie。

解决方案:在发送页面的视图上使用 ensure_csrf_cookie()


Contrib 和可重用的应用程序

由于开发者可以关闭 CsrfViewMiddleware,所以 contrib 应用程序中的所有相关视图都使用 csrf_protect 装饰器来确保这些应用程序对 CSRF 的安全性。 建议需要相同保证的其他可重用应用程序的开发人员也在他们的视图上使用 csrf_protect 装饰器。


常见问题

发布任意 CSRF 令牌对(cookie 和 POST 数据)是否存在漏洞?

不,这是设计使然。 如果没有中间人攻击,攻击者就无法将 CSRF 令牌 cookie 发送到受害者的浏览器,因此成功的攻击需要通过 XSS 或类似方式获取受害者浏览器的 cookie,在这种情况下,攻击者通常不需要 CSRF 攻击。

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


默认情况下,Django 的 CSRF 保护未链接到会话是否有问题?

不,这是设计使然。 不将 CSRF 保护链接到会话允许在诸如 pastebin 之类的站点上使用保护,这些站点允许没有会话的匿名用户提交。

如果您希望在用户会话中存储 CSRF 令牌,请使用 :setting:`CSRF_USE_SESSIONS` 设置。


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

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