跨站请求伪造(CSRF)防御

本文是 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 防御:

  1. (settings.py中的) Middleware 设置默认激活 CSRF 中间件。如果重写这个设置则需要将 ‘django.middleware.csrf.CsrfViewMiddleware' 放在任何视图处理中间件之前,以确保 CSRF 防御正常工作。

    如果你禁用了这个中间件(不推荐这样做),可以为需要保护的视图使用 csrf_protect() 装饰器。

  2. 对于任何指向内部 URL 的 POST 表单的模板, <form> 元素需要包含 csrf_token 模板标签,即:

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

指向外部 URL 的 POST 表单则不能使用 csrf_token 模板标签,因为这样会造成 CSRF token 泄露,从而导致危险。

  1. 在对应的视图函数中,一定要使用 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 防御基于以下条件:

  1. CSRF cookie基于其它网站无法访问的随机密码。

    CsrfViewMiddleware 后端设置 CSRF cookie 。如果 request 中没有相应设置 ,每个调用django.middleware.csrf.get_token()(用于获得 CSRFtoken 的内部方法)的响应都会进行设置。

    为了防御 BREACH 攻击,token 不是简单的密码,它还包含加密和解密的随机秘钥。

    为了安全起见,每次用户登录都会更改密码。

  2. 所有 POST 请求表单都包含一个名为 csrfmiddlewaretoken 的隐藏字段,这个字段的值也是使用秘钥进行加密和解密的密码。每次调用 get_token() 都会重新生成秘钥,从而保证每次响应的表单字段值都会发生变化。

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

  3. 除了 GET, HEAD, OPTIONS 或 TRACE 请求,其它请求必须设置 CSRF cookie ,并且必须设置 csrfmiddlewaretoken 字段而且必须正确。否则,用户将看到 403 错误。

    验证 csrfmiddlewaretoken 字段的值时,只对 token 和 cookie 中的密码进行比较。验证允许每次使用不同的token 。每个请求可以使用自己的 token 。

    这项检查由 CsrfViewMiddleware 完成。

  4. 另外,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 ,我们需要重新加载这些页面。 如果用户在登录后使用后退按钮,或者他们在不同的浏览器标签中进行登录,则可能发生这种情况。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容