跨站请求伪造保护

CSRF中间件和模板标签提供对跨站请求伪造简单易用的防护。 某些恶意网站上包含链接、表单按钮或者JavaScript,它们会利用登录过的用户在浏览器中的认证信息试图在你的网站上完成某些操作,这就是跨站请求伪造。 还有另外一种相关的攻击叫做“登录CSRF”,攻击站点触发用户浏览器用其它人的认证信息登录到其它站点。

针对CSRF攻击的第一个防御措施是确保GET请求(以及由RFC 7231#section-4.2.1定义的其它“安全”方法)不会带来副作用。 然后“不安全”方法(如POST、PUT和DELETE)的请求可以通过以下步骤来保护。

如何使用它

要在你的视图中使用CSRF防护,请遵循以下步骤:

  1. 默认情况下,在MIDDLEWARE设置中激活CSRF中间件。 如果你要覆盖这个设置,请记住'django.middleware.csrf.CsrfViewMiddleware'应该位于其它任何假设CSRF已经处理过的视图中间件之前。

    如果你关闭了它,虽然不建议,你可以在你想要保护的视图上使用csrf_protect()(见下文)。

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

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

    它不应该用于目标是外部URL的POST表单,因为这将引起CSRF信息泄露而导致出现漏洞。

  3. 在相应的视图函数中,确保使用RequestContext来渲染响应,这样{% csrf_token %}才能正常工作。 如果使用render()函数、通用视图或contrib应用,就已经这么做了,因为它们都使用RequestContext

AJAX

虽然上面的方法可以用于AJAX POST请求,但是它不太方便:你必须记住在每个POST请求的数据中传递CSRF token。 由于这个原因,还有另外一种方法:在每个XMLHttpRequest 上设置一个自定义的X-CSRFToken 头部,其值为CSRF token。 这非常容易,因为许多JavaScript 框架都提供在每个请求上设置头部的方法。

首先,你必须获得CSRF token。 如何做到这一点取决于是否启用了CSRF_USE_SESSIONS设置。

如果CSRF_USE_SESSIONSFalse

建议从csrftoken Cookie 中获取,如果你在视图中启用CSRF 防护它就会设置。

CSRF token 的Cookie 默认叫做csrftoken,你可以通过CSRF_COOKIE_NAME 设置自定义它的名字。

默认情况下,CSRF头部的名称为HTTP_X_CSRFTOKEN,但可以使用CSRF_HEADER_NAME设置进行自定义。

获取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。 无论如何,如果DOM中有token,Cookie中也保证有,因此你应该使用cookie!

警告

如果你的视图渲染的模板没有包含csrf_token标签,Django可能不会在Cookie中设置CSRF token。 这常见于表单是动态的方式添加到网页中的。 为了解决这个问题,Django 提供一个视图装饰器ensure_csrf_cookie(),它将强制设置这个Cookie。

如果CSRF_USE_SESSIONSTrue

如果激活CSRF_USE_SESSIONS,则必须将CSRF token包含在HTML中,并使用JavaScript从DOM读取token:

{% csrf_token %}
<script type="text/javascript">
// using jQuery
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
</script>

在AJAX请求上设置token

最后,你必须在AJAX请求中设置头部,在jQuery 1.5.1和更新版本中,请使用settings.crossDomain防止CSRF token发送给其它域:

function csrfSafeMethod(method) {
    // 这些HTTP方法不要求CSRF包含
    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和更新版本,则可以使用cookie和头部名称配置$httpProvider就可以:

$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';

在Jinja2模板中使用CSRF

Django的Jinja2模板后端将{{ csrf_input }}添加到所有模板上下文中,相当于Django模板语言中的{% csrf_token %} 像这样:

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

装饰方法

可以对需要保护的特定视图使用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”响应。 这通常只有在有一个真正的跨站点请求伪造,或由于编程错误,CSRF token未包括在POST表单中的情况下才会看到。

但是,这个错误页面不是很友好,所以你可能想提供自己的视图来处理这种情况。 为此,只需设置CSRF_FAILURE_VIEW设置即可。

CSRF失败以警告级别记录在django.security.csrf日志中。

在Django 1.11中的更改:

在旧版本中,CSRF失败记录到django.request日志中。

它是如何工作的

跨站伪造保护基于以下几点:

  1. 一个基于随机secret值的CSRF cookie,其它站点无法获取到。

    此Cookie由CsrfViewMiddleware设置。 它和每个响应一起发送,如果请求上没有设置,则调用django.middleware.csrf.get_token()(这个函数用于内部获取CSRF token)。

    为了防止BREACH攻击,token不仅仅是secret;一个随机的salt被添加到secret中,并用来加扰它。

    出于安全考虑,每当用户登录时,secret的值都会更改。

  2. 所有传出POST表单中都有一个名为“csrfmiddlewaretoken”的隐藏表单字段。 该字段的值还是这个secret的值,其中添加了salt并且用于加扰它。 在每次调用get_token()时重新生成salt,所以在每个响应中这个表单字段值都会改变。

    此部分由模板标记完成。

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

    当验证'csrfmiddlewaretoken'字段值时,只将secret而不是完整的token与cookie值中的secret进行比较。 这允许使用不断变化的token。 虽然每个请求可能使用自己的token,但是secret对所有人来说都是相同的。

    此检查由CsrfViewMiddleware完成。

  4. 此外,对于HTTPS请求,CsrfViewMiddleware会执行严格的referer检查。 这意味着即使子域可以设置或修改域上的Cookie,也不能强制用户POST给你的应用,因为该请求不是来自精确的域。

    这也解决了在使用会话独立机密时在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设置扩展接受的引荐者超出当前主机或Cookie域。

这样可以确保只有源自可信域的表单才能用于POST数据。

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

CSRF保护不能防止中间人攻击,因此使用HTTP Strict Transport Security使用HTTPS It also assumes validation of the HOST header and that there aren’t any cross-site scripting vulnerabilities on your site (because XSS vulnerabilities already let an attacker do anything a CSRF vulnerability allows and much worse).

在Django更改1.10:

添加了对令牌的盐化,并开始更改每个请求以防止BREACH攻击。

缓存

如果模板使用了csrf_token模板标签(或者其它方式调用了get_token函数),则CsrfViewMiddleware将添加一个cookie和 Vary: Cookie 头部到响应。 这意味着如果按照指示使用中间件(UpdateCacheMiddleware在所有其他中间件之前),则这个中间件将与高速缓存中间件良好协作。

但是,如果在单个视图上使用缓存装饰器,CSRF中间件将无法设置Vary头或CSRFcookie,并且响应将被缓存且没有它们中的任何一个头部。 在这种情况下,在任何需要插入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):
    ...

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

测试

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

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

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

限制¶ T0>

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

边缘案例

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

实用程序

下面的示例假设您使用基于函数的视图。 如果您使用基于类的视图,则可以参考Decorating class-based views

csrf_exempt(view)[source]

这个装饰器将视图标记为不受中间件保护的保护。 例如:

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_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)

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

场景¶ T0>

应该禁用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() (即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被发送。

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

Contrib and reusable apps

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

常见问题

发布一个任意的CSRF令牌对(cookie和POST数据)一个漏洞?

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

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

这是Django的CSRF保护在默认情况下没有连接到会话的问题吗?

不,这是设计。 不将CSRF保护与会话链接允许在允许不具有会话的匿名用户提交的站点上使用保护,例如pastebin

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

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

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