跨站请求伪造保护
CSRF 中间件和模板标签提供了易于使用的保护,防止 跨站请求伪造 。 当一个恶意网站包含一个链接、一个表单按钮或一些 JavaScript,目的是在你的网站上执行一些操作,使用在浏览器中访问恶意网站的登录用户的凭证时,就会发生这种类型的攻击。 此外,还包括一种相关的攻击类型,“登录 CSRF”,即攻击网站欺骗用户的浏览器使用他人的凭证登录网站。
对 CSRF 攻击的第一道防线是确保 GET 请求(和其他“安全”方法,如 RFC 7231#section-4.2.1 所定义的)没有副作用。通过“不安全”方法的请求,如 POST、PUT 和 DELETE,则可以通过以下步骤来保护。
如何使用它
要在你的视图中利用 CSRF 保护,请遵循以下步骤:
CSRF 中间件默认在 MIDDLEWARE 配置中被激活。如果你覆盖了这个配置,请记住
'django.middleware.csrf.CsrfViewMiddleware'
应该排在任何假设 CSRF 攻击已经被处理的视图中间件之前。如果你禁用了它,这并不推荐,你可以使用 csrf_protect() 对你想要保护的特定视图进行保护(见下文)。
在任何使用 POST 表单的模板中,如果表单是针对内部 URL 的,请在
<form>
元素中使用 csrf_token 标签,例如:<form method="post">{% csrf_token %}
对于以外部 URL 为目标的 POST 表单,不应该这样做,因为这会导致 CSRF 令牌泄露,从而导致漏洞。
在相应的视图函数中,确保 RequestContext 用于渲染响应,这样
{% csrf_token %}
才能正常工作。如果你使用的是 render() 函数、通用视图或 contrib 应用程序,你已经被覆盖了,因为这些都使用RequestContext
。
AJAX
虽然上述方法可以用于 AJAX POST 请求,但它有一些不便之处:你必须记住在每个 POST 请求中都要把 CSRF 令牌作为 POST 数据传递进来。出于这个原因,有一种替代方法:在每个 XMLHttpRequest 上,设置一个自定义的 X-CSRFToken
头(由 CSRF_HEADER_NAME 设置指定)为 CSRF 标记的值。这通常比较容易,因为许多 JavaScript 框架提供了钩子,允许在每个请求中设置头。
首先,你必须获得 CSRF 令牌。如何做取决于 CSRF_USE_SESSIONS 和 CSRF_COOKIE_HTTPONLY 配置是否启用。
当 CSRF_USE_SESSIONS 和 CSRF_COOKIE_HTTPONLY 为 False
时获取令牌
推荐的令牌来源是 csrftoken
cookie,如果你已经为你的视图启用了上文所述的 CSRF 保护,则会设置该 cookie。
CSRF 令牌 cookie 默认命名为 csrftoken
,但你可以通过 CSRF_COOKIE_NAME 配置来控制 cookie 的名称。
你可以通过这样的方式获得令牌:
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
上述代码可以通过使用 JavaScript Cookie 库 代替 getCookie
来简化:
const csrftoken = Cookies.get('csrftoken');
注解
CSRF 令牌也存在于 DOM 中,但只有在模板中使用 csrf_token 明确包含时才会出现。Cookie 包含了规范的令牌;CsrfViewMiddleware
更倾向于使用 cookie 而不是 DOM 中的令牌。无论如何,如果 DOM 中存在令牌,你就能保证拥有 cookie,所以你应该使用 cookie!
警告
如果你的视图没有渲染包含 csrf_token 模板标签的模板,Django 可能不会设置 CSRF 令牌 cookie。这种情况常见于表单被动态添加到页面的情况。针对这种情况,Django 提供了一个视图装饰器来强制设置 cookie: sure_csrf_cookie()
。
当 CSRF_USE_SESSIONS 或 CSRF_COOKIE_HTTPONLY 为 True
时获取令牌
如果你激活了 CSRF_USE_SESSIONS 或 CSRF_COOKIE_HTTPONLY,你必须在你的 HTML 中包含 CSRF 令牌,并通过 JavaScript 从 DOM 中读取该令牌:
{% csrf_token %}
<script>
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
</script>
在 AJAX 请求中设置令牌
最后,你需要在 AJAX 请求中设置头。使用 fetch() API:
const request = new Request(
/* URL */,
{
method: 'POST',
headers: {'X-CSRFToken': csrftoken},
mode: 'same-origin' // Do not send CSRF token to another domain.
}
);
fetch(request).then(function(response) {
// ...
});
在 Jinja2 模板中使用 CSRF
Django 的 Jinja2 模板后端在所有模板的上下文中添加了 {{ csrf_input }}
,相当于 Django 模板语言中的 {% csrf_token %}
。例如:
<form method="post">{{ csrf_input }}
装饰器方法
与其添加 CsrfViewMiddleware
作为全面保护,不如在需要保护的特定视图上使用 csrf_protect
装饰器,它具有完全相同的功能。它必须用于 同时 在输出中插入 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 表单中。
然而,错误页面不是很友好,所以你可能想提供自己的视图来处理这种情况。 要做到这一点,请设置 CSRF_FAILURE_VIEW 配置。
CSRF 失败会被记录为警告到 django.security.csrf 记录器。
工作方式
CSRF 保护是基于以下几点:
一个基于随机密钥值的 CSRF cookie,其他网站无法访问。
这个 cookie 是由
CsrfViewMiddleware
设置的。如果在请求中还没有设置的话,那么它将与调用了django.middleware.csrf.get_token()
(内部用于获取 CSRF 令牌的函数)的每个响应一起发送。为了防止 BREACH 攻击,令牌不是简单的密钥,而是在密钥前面加上一个随机掩码,用来扰乱密钥。
出于安全考虑,每次用户登录时都会改变密钥的值。
一个隐藏的表单字段,名称为“csrfmiddlewaretoken”,存在于所有发出的 POST 表单中。这个字段的值也是密钥的值,但有一个掩码,这个掩码会被添加到字段中,并被用来扰乱字段。掩码在每次调用
get_token()
时都会重新生成,所以表单字段的值在每次响应时都会改变。这一部分是由模板标签来完成的。
对于所有不使用 HTTP GET、HEAD、OPTIONS 或 TRACE 的传入请求,必须存在一个 CSRF cookie,并且“csrfmiddlewaretoken”字段必须存在且正确。如果不存在,用户将得到一个 403 错误。
当验证“csrfmiddlewaretoken”字段值时,只有密钥,而不是完整的令牌,会与 cookie 值中的密钥进行比较。这允许使用不断变化的令牌。虽然每个请求都可能使用自己的令牌,但密钥对所有请求都是通用的。
这个检查是由
CsrfViewMiddleware
完成的。CsrfViewMiddleware
verifies the Origin header, if provided by the browser, against the current host and the CSRF_TRUSTED_ORIGINS setting. This provides protection against cross-subdomain attacks.In addition, for HTTPS requests, if the
Origin
header isn’t provided,CsrfViewMiddleware
performs strict referer checking. This means that even if a subdomain can set or modify cookies on your domain, it can’t force a user to post to your application since that request won’t come from your own exact domain.这也解决了在 HTTPS 下使用独立于会话的密钥时可能出现的中间人攻击问题,这是因为 HTTP
Set-Cookie
头会被客户接受(不幸的是),即使他们在 HTTPS 下与一个网站对话。对 HTTP 请求不进行 Referer 检查,因为 HTTP 下Referer
头的存在不够可靠)。如果设置了 CSRF_COOKIE_DOMAIN 设置,则会将 referer 与之进行比较。你可以通过包含一个前导点号来允许跨子域请求。例如,
CSRF_COOKIE_DOMAIN = '.example.com'
将允许来自www.example.com
和api.example.com
的 POST 请求。如果没有设置,那么 referer 必须与 HTTPHost
头匹配。通过 CSRF_TRUSTED_ORIGINS 设置,可以将接受的 referer 扩展到当前主机或 cookie 域之外。
New in Django 4.0:
Origin
checking was added, as described above.
这确保了只有源自受信任域的表单才能用于 POST 回数据。
它故意忽略了 GET 请求(以及被 RFC 7231#section-4.2.1 定义为“安全”的其他请求)。RFC 7231#section-4.2.1 将 POST、PUT 和 DELETE 定义为“不安全”,所有其他方法也被认为是不安全的,以获得最大的保护。
CSRF 保护不能防止中间人攻击,所以使用 HTTPS 与 HTTP 严格传输安全。它还假设 验证 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" ...>"
这样的替代品来链接第三方网站。
缓存
如果 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
通常会成为测试视图功能的一大障碍,因为需要 CSRF 令牌,而 CSRF 令牌必须在每个 POST 请求中发送。 出于这个原因,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
这样的等价物没有运行, 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 令牌。
解决方法:使用 requests_csrf_token()
。
不受保护的视图需要 CSRF 令牌
可能有一些视图是不受保护的,已经被 csrf_exempt
豁免,但仍然需要包括 CSRF 令牌。
解决方法:使用 csrf_exempt() 后面跟着 requires_csrf_token()。(即 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 请求,而该页面并没有一个带有 csrf_token 的 HTML 表单,这将导致所需的 CSRF cookie 被发送。
解决方法:在发送页面的视图上使用 sure_csrf_cookie()
。
Contrib 和可重用的应用
因为开发者可以关闭 CsrfViewMiddleware
,所以 contrib 应用程序中的所有相关视图都使用 csrf_protect
装饰器来确保这些应用程序对 CSRF 的安全性。 建议其他可重用应用的开发者,如果想要得到同样的保证,也在他们的视图上使用 csrf_protect
装饰器。
配置
一些配置可以用来控制Django 的 CSRF 行为:
- CSRF_COOKIE_AGE
- CSRF_COOKIE_DOMAIN
- CSRF_COOKIE_HTTPONLY
- CSRF_COOKIE_NAME
- CSRF_COOKIE_PATH
- CSRF_COOKIE_SAMESITE
- CSRF_COOKIE_SECURE
- CSRF_FAILURE_VIEW
- CSRF_HEADER_NAME
- CSRF_TRUSTED_ORIGINS
- CSRF_USE_SESSIONS
常问问题
可以提交任意的 CSRF 令牌对(cookie 和 POST 数据)是漏洞吗?
不,这是设计好的。如果没有中间人攻击,攻击者就没有办法向受害者的浏览器发送 CSRF 令牌 cookie,所以成功的攻击需要通过 XSS 或类似的方式获得受害者浏览器的 cookie,在这种情况下,攻击者通常不需要 CSRF 攻击。
一些安全审计工具将此标记为问题,但如前所述,攻击者无法窃取用户浏览器的 CSRF cookie。使用 Firebug、Chrome 开发工具等“窃取”或修改 自己的 令牌并不是漏洞。
Django 的 CSRF 保护默认不与会话关联,是不是有问题?
不,这是设计好的。不将 CSRF 保护与会话联系起来,就可以在诸如 pastebin 这样允许匿名用户提交的网站上使用保护,而这些用户并没有会话。
如果你希望在用户的会话中存储 CSRF 令牌,请使用 CSRF_USE_SESSIONS 设置。
为什么用户登录后会遇到 CSRF 验证失败?
出于安全考虑,每次用户登录时,CSRF 令牌都会轮换。任何在登录前生成表单的页面都会有一个旧的、无效的 CSRF 令牌,需要重新加载。如果用户在登录后使用后退按钮或在不同的浏览器标签页中登录,可能会发生这种情况。