本文是 Django 官网文档的翻译。
官网链接:https://docs.djangoproject.com/en/1.11/ref/csrf/
适用版本:Django1.11
CSRF 中间件结合模板标签实现 CSRF 防御。恶意网站通过登录到我们网站的用户(同时在浏览器中访问了恶意网站)的凭证以链接、表单按钮或 JavaScript 的形式对我们的网站进行操作的攻击称为 CSRF 攻击。CSRF 防御还可以防御‘登录 CSRF ’攻击(恶意网站使用其他人的凭证欺骗用户浏览器实现登录)。
CSRF 防御首先要保证 GET 请求(及 RFC-7321#section-4.2.1 定义的其它‘安全’方法)安全。‘不安全’方法的请求(如 POST 、PUT 和 DELETE )可以采用以下步骤进行防御。
如何使用
通过以下步骤实现视图的 CSRF 防御:
-
(settings.py中的) Middleware 设置默认激活 CSRF 中间件。如果重写这个设置则需要将 ‘django.middleware.csrf.CsrfViewMiddleware' 放在任何视图处理中间件之前,以确保 CSRF 防御正常工作。
如果你禁用了这个中间件(不推荐这样做),可以为需要保护的视图使用 csrf_protect() 装饰器。
对于任何指向内部 URL 的 POST 表单的模板, <form> 元素需要包含 csrf_token 模板标签,即:
<form action="" method="post">{% csrf_token %}
指向外部 URL 的 POST 表单则不能使用 csrf_token 模板标签,因为这样会造成 CSRF token 泄露,从而导致危险。
-
在对应的视图函数中,一定要使用 RequestContext 渲染 Response,以保证 {% csrf_token %} 正常工作。如果使用 render() 函数、通用视图、或者contrib app 渲染 Response,那么不用考虑这个问题,因为它们使用RequestContext 。
AJAX
虽然上面的方法可用于 AJAX POST 请求,但却不太方便:我们需要为每个 POST 请求的 POST 数据加入 CSRF token 。为了解决这个问题,我们提供了一种替代方法:为每个 XMLHttpRequest 设置一个自定义 X-CSRFToken标头保存 CSRF token 。由于许多 JavaScript 框架提供为每个请求设置标头的 hooks,这样做会简单的多。
首先,我们必须获取 CSRF token 。获取方法取决于 CSRF_USE_SESSIONS 设置的值。
CSRF_USE_SESSION 为False ,获取token
翻译补充:
CSRF_USE_SESSION 为False 表示 csrftoken 保存在csrftoken cookie 中。
如果您如上所述为视图开启了 CSRF 防御,django 将设置 csrftoken cookie,因此,这种情况推荐使用 csrftoken cookie 获取 csrf token。
注意:
CSRF token cookie 的默认名称为 csrftoken ,但可以通过设置 CSRF_COOKIE_NAME 来更改 cookie 名称。
CSRF 标头的默认名称为 HTTP_X_CSRFTOKEN ,但也可以通过设置 CSRF_HEADER_NAME 来自定义 CSRF 标头名称。
获得 token 的方法很简单:
// using jQuery
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// 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;
}
var csrftoken = getCookie('csrftoken');
我们可以使用 JavaScript Cookie 库实现getCookie 函数的功能,从而实现简化:
var csrftoken = Cookies.get('csrftoken');
注意:
模板明确包含 csrf_token 时,DOM 中也包含 CSRF token。 cookie 包含规范 token ; 与在 DOM 中获取 token 相比,CsrfViewMiddleware 倾向于从 cookie 中获取 token 。如果 DOM 中包含 token ,那么 cookie 一定包含 token,因此我们应该使用 cookie !
警告:
如果你的视图没有渲染包含 csrf_token 模板标签的模板。Django 可能不会在 cookie 中设置 CSRF token 。自动添加表单的页面通常会存在这种情况。为了解决这个问题,django 提供了强制设置 cookie 的视图装饰器 ensure_csrf_cookie() 。
CSRF_USE_SESSION为True,获取 token
翻译补充:
CSRF_USE_SESSION 为True 表示使用 csrf token保存在 session 中。
如果激活了 CSRF_USE_SESSION , HTML 中必须包含 CSRF token,并使用 JavaScript 读取 DOM中的 token :
{% csrf_token %}
<script type="text/javascript">
// using jQuery
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
</script>
设置 AJAX 请求的 token
最后,我们需要设置 AJAX 请求的标头,jQuery1.5.1 及之上版本可以通过设置 settings.crossDomain 来防止将 CSRF token 发送到其它域名:
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
如果使用 AngularJS 1.1.3 及之上版本,只需要为 $http提供者 配置 cookie和标头名称:
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
在Jinja2模板中使用CSRF
Django 的 Jinja2 模板后端为所有模板的context添加 {{ csrf_input }} ,{{ csrf_input }} 与 Django 模板语言中的{% csrf_token%} 等价。比如:
<form action="" method="post">{{ csrf_input }}
装饰器方法
除了使用 CsrfViewMiddleware 对所有试题进行保护,我们也可以为需要保护的特定视图添加 csrf_protect 装饰器(与 CsrfViewMiddleware 实现的功能相同)。输出包含 CSRF token 的视图和接收该视图的 POST 数据的视图(这个视图通常通过同一个视图函数实现,但也有例外)必须都需要添加装饰器。
不推荐单独使用装饰器,这样很容易由于忘记使用而造成安全隐患。 最好采用“万无一失”的策略,也就是同时使用,这样将产生最小的开销。
csrf_protect(view)
为视图提供 CsrfViewMiddleware 保护的装饰器。
用法:
from django.views.decorators.csrf import csrf_protect
from django.shortcuts import render
@csrf_protect
def my_view(request):
c = {}
# ...
return render(request, "a_template.html", c)
如果使用基类视图,可以参考装饰类视图。
拒绝请求
默认情况下,如果请求没有通过 CsrfViewMiddleware 检查,用户会看到 '403 Forbidden' 响应。通常只有存在真正的跨站点请求伪造、或者由于编程错误没有在 POST 表中 添加 CSRF token 才能看到这种情况。
然而,错误页面非常不友好,因此你可能希望为这种情况提供自己的视图,只需设置 CSRF_FAILURE_VIEW 即可为拒绝请求响应提供视图。
我们可以在 django.security.csrf logger 的 warnings 等级的记录中查看 CSRF 失败记录。
Django1.11的变化:
旧版本中,django.request logger记录 CSRF 失败。
如何工作
CSRF 防御基于以下条件:
-
CSRF cookie基于其它网站无法访问的随机密码。
CsrfViewMiddleware 后端设置 CSRF cookie 。如果 request 中没有相应设置 ,每个调用django.middleware.csrf.get_token()(用于获得 CSRFtoken 的内部方法)的响应都会进行设置。
为了防御 BREACH 攻击,token 不是简单的密码,它还包含加密和解密的随机秘钥。
为了安全起见,每次用户登录都会更改密码。
-
所有 POST 请求表单都包含一个名为 csrfmiddlewaretoken 的隐藏字段,这个字段的值也是使用秘钥进行加密和解密的密码。每次调用 get_token() 都会重新生成秘钥,从而保证每次响应的表单字段值都会发生变化。
这一部分由模板标签完成。
-
除了 GET, HEAD, OPTIONS 或 TRACE 请求,其它请求必须设置 CSRF cookie ,并且必须设置 csrfmiddlewaretoken 字段而且必须正确。否则,用户将看到 403 错误。
验证 csrfmiddlewaretoken 字段的值时,只对 token 和 cookie 中的密码进行比较。验证允许每次使用不同的token 。每个请求可以使用自己的 token 。
这项检查由 CsrfViewMiddleware 完成。
-
另外,CsrfViewMiddleware 对 HTTPS 请求进行更加严格的检查。这意味着即使可以设置和更改 cookie 的子域也不能向应用进行 POST 请求,这是由于请求来自于不同的域。
这也解决使用会话独立密码时 HTTPS 可能引发的 man-in-the-middle 攻击,这是由于即使正在与 HTTPS 站点通话,HTTP客户端也可以接收HTTP Set-Cookie 标头( HTTP 请求不进行 Referer 检查,因为 HTTP 中的 Referer 头不很可靠)。
如果设置了 CSRF_COOKIE_DOMAIN,则将对 referer 和 CSRF_COOKIE_DOMAIN 进行比较。 这个设置支持子域。 比如
CSRF_COOKIE_DOMAIN ='.example.com'
将允许 www.example.com 和 api.example.com 的 POST 请求。 如果没有设置,referer 必须与 HTTP Host 头匹配。CSRF_TRUSTED_ORIGINS 设置将 referers 扩展到当前主机或 cookie 域以外。
这样可以保证只有来自受信任域的表单才能 POST 数据。
这显然忽略了 GET 请求(以及其他 RFC7231 定义的请求),这些请求应该永远不存在任何潜在的副作用,因此 CSRF 攻击对 GET请求应该是无害的。RFC7231定义的 POST、PUT 和 DELETE 是不安全的,为了实现最大限度的包含,所有其它的方法也是不安全的。
CSRF 防御不能抵挡 man-in-the-middle 攻击,因此,请使用 HTTPS 。这里还假设 HOST 标头验证以及网站没有任何跨网站脚本漏洞( XSS 漏洞比 CSRF 漏洞更加致命)。
Django1.10的变化:
为 token 设置秘钥,并且开始为每次请求更改 token 以避免 BREACH 攻击。
缓存
如果模板使用 csrf_token 模板标签(或者采用其它方式调用 get_token 函数),CSRFViewMiddleware 将为响应增加一个 cookie 和一个 Vary:Cookie 标头。这意味着,如果按照顺序使用中间件( UpdateCacheMiddleware 位于所有其它中间件的前面), CSRFViewMiddleware 中间件将与缓存中间件配合良好。
但是,如果对个别视图使用缓存装饰器,CSRF 中间件可能还没有设置 Vary 标头或者 SCRF cookie ,因此可能会缓存没有设置这两项的响应。在这种情况下,任何需要使用 CSRF token 的视图都应该先使用 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):
...
如果使用类视图,请参考装饰类视图。
测试
由于每个 POST 请求都需要发送 CSRF token ,CsrfViewMiddleware 通常会成为测试视图功能的一大障碍。因此,Django HTTP 测试客户端为请求设置了标志位,这个标志位解除了中间件和 csrf_protect 装饰器的要求,这样视图将不再拒绝请求。在其它方面(例如发送 cookies )Django HTTP 测试客户端的行为是一样的。
如果由于某种原因,希望测试客户端进行 CSRF 检查,我们可以创建一个设置强制 CSRF 检查的测试客户端实例:
>>> from django.test import Client
>>> csrf_client = Client(enforce_csrf_checks=True)</pre>
限制
在客户端,网站的子域可以为整个网站设置 cookie 。通过设置 cookie 并使用相应的 token ,子域可以绕过 CSRF 防御。避免这种情况的唯一办法是只允许受信任的用户控制子域(至少不允许其他用户设置 cookie )。还要注意的是,即使没有 CSRF 攻击,还可能会有当前浏览器不容许修复的其他漏洞(比如会话修复),这种情况下,允许不受信任的用户控制子域将会非常危险。
特殊情况
有些视图使用不常用的请求,这将意味着这些视图不符合上述正常模式。在这些情况下可以使用许多方法,下面的内容描述了可能存在的场景。
用法
下面的例子使用函数视图,如果使用类视图,请参考装饰类视图。
csrf_exempt(view)
这个装饰器表示视图不使用中间件,比如:
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
@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_token 模板标签正常工作。这个装饰器的工作原理与 csrf_protect 类似,但是不会拒绝请求,例如:
from django.views.decorators.csrf import requires_csrf_token
from django.shortcuts import render
@requires_csrf_token
def my_view(request):
c = {}
# ...
return render(request, "a_template.html", c)
ensure_csrf_cookie(view)
这个装饰器强制视图发送 CSRF cookie 。
场景
只有少数视图需要禁用 CSRF 防御
大多数视图需要使用 CSRF 防御,但是少数视图不用。
解决方案:使用 CsrfViewMiddleware 中间件并为需要禁用 CSRF 防御的视图使用 csrf_exempt() 。
没有使用 CsrfViewMiddleware.process_view
可能存在这种情况:视图运行之前 CsrfViewMiddleware.process_view 没有运行(比如400和500),但是表单需要使用 CSRF 防御。
解决方案:使用 requires_csrf_token 。
不用保护的视图需要 SCRF token
可能有一些视图不需要 csrf 防御并使用了 csrf_exempt 装饰器,但是该视图需要使用 CSRF token 。
解决方案:在 csrf_exempt() 之前使用 requires_csrf_token()(也就是说,requires_csrf_token 装饰器放在最内层)。
只需要保护视图的一条路径
视图只在一种情况下需要使用 CSRF 防御,而且其他情况不能使用 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 cookie 功能的 csrf_token 表单字段。
解决方案:在发送视图中使用 ensure_csrf_cookie() 。
contrib 和可复用 Apps
开发过程中可能需要关闭 CsrfViewMiddleware
, contrib 中所有需要 CSRF 防御的视图都使用 csrf_protect 装饰器。推荐其他开发可复用应用的开发者为需要 CSRF 防御的视图添加 csrf_protect 装饰器。
设置
一些设置可以控制 Django 的 CSRF 行为:
常见问题
任意发送的 CSRF token 对( cookie 和 POST 数据)是一个漏洞吗?
不是,这是设计好的。除了 man-in-the-middle 攻击 ,攻击者无法将 CSRF token cookie 发送到受害者的浏览器。因此,成功进行攻击需要通过 XSS 或类似的方式获得受害者浏览器的 cookie ,在这种情况下,攻击者通常不需要进行 CSRF 攻击。
一些安全检查工具认为这是个安全问题。但如前所述,攻击者不能窃取用户浏览器的 CSRF cookie 。使用Firebug、Chrome 开发工具等“窃取”或修改自己的 token 不是一个漏洞。
Django 的 CSRF 防御不会默认链接到会话(session)是一个问题吗?
不是,这是设计好的。CSRF 防御与会话(session) 不进行链接将允许我们对允许匿名用户提交表单的网站(诸如 pastebin 之类)进行保护,这种网站的匿名用户没有会话(session)。
如果您需要将 CSRF token 存储在用户的会话中,请设置 CSRF_USE_SESSIONS。
为什么用户登录后还会遇到 CSRF 验证失败?
出于安全原因,用户登录时会轮换 CSRF token 。任何在登录之前生成表单的页面都会包含一个旧的、无效的 CSRF token ,我们需要重新加载这些页面。 如果用户在登录后使用后退按钮,或者他们在不同的浏览器标签中进行登录,则可能发生这种情况。