跨站請求偽造(CSRF)防御

本文是 Django 官網(wǎng)文檔的翻譯。

官網(wǎng)鏈接:https://docs.djangoproject.com/en/1.11/ref/csrf/

適用版本:Django1.11

CSRF 中間件結(jié)合模板標(biāo)簽實(shí)現(xiàn) CSRF 防御。惡意網(wǎng)站通過登錄到我們網(wǎng)站的用戶(同時在瀏覽器中訪問了惡意網(wǎng)站)的憑證以鏈接、表單按鈕或 JavaScript 的形式對我們的網(wǎng)站進(jìn)行操作的攻擊稱為 CSRF 攻擊。CSRF 防御還可以防御‘登錄 CSRF ’攻擊(惡意網(wǎng)站使用其他人的憑證欺騙用戶瀏覽器實(shí)現(xiàn)登錄)。

CSRF 防御首先要保證 GET 請求(及 RFC-7321#section-4.2.1 定義的其它‘安全’方法)安全?!话踩椒ǖ恼埱螅ㄈ?POST 、PUT 和 DELETE )可以采用以下步驟進(jìn)行防御。

如何使用


通過以下步驟實(shí)現(xiàn)視圖的 CSRF 防御:

  1. (settings.py中的) Middleware 設(shè)置默認(rèn)激活 CSRF 中間件。如果重寫這個設(shè)置則需要將 ‘django.middleware.csrf.CsrfViewMiddleware' 放在任何視圖處理中間件之前,以確保 CSRF 防御正常工作。

    如果你禁用了這個中間件(不推薦這樣做),可以為需要保護(hù)的視圖使用 csrf_protect() 裝飾器。

  2. 對于任何指向內(nèi)部 URL 的 POST 表單的模板, <form> 元素需要包含 csrf_token 模板標(biāo)簽,即:

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

指向外部 URL 的 POST 表單則不能使用 csrf_token 模板標(biāo)簽,因?yàn)檫@樣會造成 CSRF token 泄露,從而導(dǎo)致危險(xiǎn)。

  1. 在對應(yīng)的視圖函數(shù)中,一定要使用 RequestContext 渲染 Response,以保證 {% csrf_token %} 正常工作。如果使用 render() 函數(shù)、通用視圖、或者contrib app 渲染 Response,那么不用考慮這個問題,因?yàn)樗鼈兪褂肦equestContext 。

    ?

AJAX

雖然上面的方法可用于 AJAX POST 請求,但卻不太方便:我們需要為每個 POST 請求的 POST 數(shù)據(jù)加入 CSRF token 。為了解決這個問題,我們提供了一種替代方法:為每個 XMLHttpRequest 設(shè)置一個自定義 X-CSRFToken標(biāo)頭保存 CSRF token 。由于許多 JavaScript 框架提供為每個請求設(shè)置標(biāo)頭的 hooks,這樣做會簡單的多。

首先,我們必須獲取 CSRF token 。獲取方法取決于 CSRF_USE_SESSIONS 設(shè)置的值。

CSRF_USE_SESSION 為False ,獲取token

翻譯補(bǔ)充:

CSRF_USE_SESSION 為False 表示 csrftoken 保存在csrftoken cookie 中。

如果您如上所述為視圖開啟了 CSRF 防御,django 將設(shè)置 csrftoken cookie,因此,這種情況推薦使用 csrftoken cookie 獲取 csrf token。

注意:

CSRF token cookie 的默認(rèn)名稱為 csrftoken ,但可以通過設(shè)置 CSRF_COOKIE_NAME 來更改 cookie 名稱。

CSRF 標(biāo)頭的默認(rèn)名稱為 HTTP_X_CSRFTOKEN ,但也可以通過設(shè)置 CSRF_HEADER_NAME 來自定義 CSRF 標(biāo)頭名稱。

獲得 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 庫實(shí)現(xiàn)getCookie 函數(shù)的功能,從而實(shí)現(xiàn)簡化:

var csrftoken = Cookies.get('csrftoken');

注意:

模板明確包含 csrf_token 時,DOM 中也包含 CSRF token。 cookie 包含規(guī)范 token ; 與在 DOM 中獲取 token 相比,CsrfViewMiddleware 傾向于從 cookie 中獲取 token 。如果 DOM 中包含 token ,那么 cookie 一定包含 token,因此我們應(yīng)該使用 cookie !

警告:

如果你的視圖沒有渲染包含 csrf_token 模板標(biāo)簽的模板。Django 可能不會在 cookie 中設(shè)置 CSRF token 。自動添加表單的頁面通常會存在這種情況。為了解決這個問題,django 提供了強(qiáng)制設(shè)置 cookie 的視圖裝飾器 ensure_csrf_cookie() 。

CSRF_USE_SESSION為True,獲取 token

翻譯補(bǔ)充:

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>

設(shè)置 AJAX 請求的 token

最后,我們需要設(shè)置 AJAX 請求的標(biāo)頭,jQuery1.5.1 及之上版本可以通過設(shè)置 settings.crossDomain 來防止將 CSRF token 發(fā)送到其它域名:

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和標(biāo)頭名稱:

$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 對所有試題進(jìn)行保護(hù),我們也可以為需要保護(hù)的特定視圖添加 csrf_protect 裝飾器(與 CsrfViewMiddleware 實(shí)現(xiàn)的功能相同)。輸出包含 CSRF token 的視圖和接收該視圖的 POST 數(shù)據(jù)的視圖(這個視圖通常通過同一個視圖函數(shù)實(shí)現(xiàn),但也有例外)必須都需要添加裝飾器。

不推薦單獨(dú)使用裝飾器,這樣很容易由于忘記使用而造成安全隱患。 最好采用“萬無一失”的策略,也就是同時使用,這樣將產(chǎn)生最小的開銷。

csrf_protect(view)

為視圖提供 CsrfViewMiddleware 保護(hù)的裝飾器。

用法:

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)

如果使用基類視圖,可以參考裝飾類視圖。

拒絕請求


默認(rèn)情況下,如果請求沒有通過 CsrfViewMiddleware 檢查,用戶會看到 '403 Forbidden' 響應(yīng)。通常只有存在真正的跨站點(diǎn)請求偽造、或者由于編程錯誤沒有在 POST 表中 添加 CSRF token 才能看到這種情況。

然而,錯誤頁面非常不友好,因此你可能希望為這種情況提供自己的視圖,只需設(shè)置 CSRF_FAILURE_VIEW 即可為拒絕請求響應(yīng)提供視圖。

我們可以在 django.security.csrf logger 的 warnings 等級的記錄中查看 CSRF 失敗記錄。

Django1.11的變化:

舊版本中,django.request logger記錄 CSRF 失敗。

如何工作


CSRF 防御基于以下條件:

  1. CSRF cookie基于其它網(wǎng)站無法訪問的隨機(jī)密碼。

    CsrfViewMiddleware 后端設(shè)置 CSRF cookie 。如果 request 中沒有相應(yīng)設(shè)置 ,每個調(diào)用django.middleware.csrf.get_token()(用于獲得 CSRFtoken 的內(nèi)部方法)的響應(yīng)都會進(jìn)行設(shè)置。

    為了防御 BREACH 攻擊,token 不是簡單的密碼,它還包含加密和解密的隨機(jī)秘鑰。

    為了安全起見,每次用戶登錄都會更改密碼。

  2. 所有 POST 請求表單都包含一個名為 csrfmiddlewaretoken 的隱藏字段,這個字段的值也是使用秘鑰進(jìn)行加密和解密的密碼。每次調(diào)用 get_token() 都會重新生成秘鑰,從而保證每次響應(yīng)的表單字段值都會發(fā)生變化。

    這一部分由模板標(biāo)簽完成。

  3. 除了 GET, HEAD, OPTIONS 或 TRACE 請求,其它請求必須設(shè)置 CSRF cookie ,并且必須設(shè)置 csrfmiddlewaretoken 字段而且必須正確。否則,用戶將看到 403 錯誤。

    驗(yàn)證 csrfmiddlewaretoken 字段的值時,只對 token 和 cookie 中的密碼進(jìn)行比較。驗(yàn)證允許每次使用不同的token 。每個請求可以使用自己的 token 。

    這項(xiàng)檢查由 CsrfViewMiddleware 完成。

  4. 另外,CsrfViewMiddleware 對 HTTPS 請求進(jìn)行更加嚴(yán)格的檢查。這意味著即使可以設(shè)置和更改 cookie 的子域也不能向應(yīng)用進(jìn)行 POST 請求,這是由于請求來自于不同的域。

    這也解決使用會話獨(dú)立密碼時 HTTPS 可能引發(fā)的 man-in-the-middle 攻擊,這是由于即使正在與 HTTPS 站點(diǎn)通話,HTTP客戶端也可以接收HTTP Set-Cookie 標(biāo)頭( HTTP 請求不進(jìn)行 Referer 檢查,因?yàn)?HTTP 中的 Referer 頭不很可靠)。

    如果設(shè)置了 CSRF_COOKIE_DOMAIN,則將對 referer 和 CSRF_COOKIE_DOMAIN 進(jìn)行比較。 這個設(shè)置支持子域。 比如 CSRF_COOKIE_DOMAIN ='.example.com' 將允許 www.example.com 和 api.example.com 的 POST 請求。 如果沒有設(shè)置,referer 必須與 HTTP Host 頭匹配。

    CSRF_TRUSTED_ORIGINS 設(shè)置將 referers 擴(kuò)展到當(dāng)前主機(jī)或 cookie 域以外。

這樣可以保證只有來自受信任域的表單才能 POST 數(shù)據(jù)。

這顯然忽略了 GET 請求(以及其他 RFC7231 定義的請求),這些請求應(yīng)該永遠(yuǎn)不存在任何潛在的副作用,因此 CSRF 攻擊對 GET請求應(yīng)該是無害的。RFC7231定義的 POST、PUT 和 DELETE 是不安全的,為了實(shí)現(xiàn)最大限度的包含,所有其它的方法也是不安全的。

CSRF 防御不能抵擋 man-in-the-middle 攻擊,因此,請使用 HTTPS 。這里還假設(shè) HOST 標(biāo)頭驗(yàn)證以及網(wǎng)站沒有任何跨網(wǎng)站腳本漏洞( XSS 漏洞比 CSRF 漏洞更加致命)。

Django1.10的變化:

為 token 設(shè)置秘鑰,并且開始為每次請求更改 token 以避免 BREACH 攻擊。


緩存


如果模板使用 csrf_token 模板標(biāo)簽(或者采用其它方式調(diào)用 get_token 函數(shù)),CSRFViewMiddleware 將為響應(yīng)增加一個 cookie 和一個 Vary:Cookie 標(biāo)頭。這意味著,如果按照順序使用中間件( UpdateCacheMiddleware 位于所有其它中間件的前面), CSRFViewMiddleware 中間件將與緩存中間件配合良好。

但是,如果對個別視圖使用緩存裝飾器,CSRF 中間件可能還沒有設(shè)置 Vary 標(biāo)頭或者 SCRF cookie ,因此可能會緩存沒有設(shè)置這兩項(xiàng)的響應(yīng)。在這種情況下,任何需要使用 CSRF token 的視圖都應(yīng)該先使用 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 請求都需要發(fā)送 CSRF token ,CsrfViewMiddleware 通常會成為測試視圖功能的一大障礙。因此,Django HTTP 測試客戶端為請求設(shè)置了標(biāo)志位,這個標(biāo)志位解除了中間件和 csrf_protect 裝飾器的要求,這樣視圖將不再拒絕請求。在其它方面(例如發(fā)送 cookies )Django HTTP 測試客戶端的行為是一樣的。

如果由于某種原因,希望測試客戶端進(jìn)行 CSRF 檢查,我們可以創(chuàng)建一個設(shè)置強(qiáng)制 CSRF 檢查的測試客戶端實(shí)例:

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

限制


在客戶端,網(wǎng)站的子域可以為整個網(wǎng)站設(shè)置 cookie 。通過設(shè)置 cookie 并使用相應(yīng)的 token ,子域可以繞過 CSRF 防御。避免這種情況的唯一辦法是只允許受信任的用戶控制子域(至少不允許其他用戶設(shè)置 cookie )。還要注意的是,即使沒有 CSRF 攻擊,還可能會有當(dāng)前瀏覽器不容許修復(fù)的其他漏洞(比如會話修復(fù)),這種情況下,允許不受信任的用戶控制子域?qū)浅NkU(xiǎn)。

特殊情況


有些視圖使用不常用的請求,這將意味著這些視圖不符合上述正常模式。在這些情況下可以使用許多方法,下面的內(nèi)容描述了可能存在的場景。

用法

下面的例子使用函數(shù)視圖,如果使用類視圖,請參考裝飾類視圖。

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 沒有運(yùn)行, csrf_token 模板標(biāo)簽將不起作用。視圖裝飾器 requires_csrf_token 用于保證 csrf_token 模板標(biāo)簽正常工作。這個裝飾器的工作原理與 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)

這個裝飾器強(qiáng)制視圖發(fā)送 CSRF cookie 。

場景

只有少數(shù)視圖需要禁用 CSRF 防御

大多數(shù)視圖需要使用 CSRF 防御,但是少數(shù)視圖不用。

解決方案:使用 CsrfViewMiddleware 中間件并為需要禁用 CSRF 防御的視圖使用 csrf_exempt() 。

沒有使用 CsrfViewMiddleware.process_view

可能存在這種情況:視圖運(yùn)行之前 CsrfViewMiddleware.process_view 沒有運(yùn)行(比如400和500),但是表單需要使用 CSRF 防御。

解決方案:使用 requires_csrf_token 。

不用保護(hù)的視圖需要 SCRF token

可能有一些視圖不需要 csrf 防御并使用了 csrf_exempt 裝飾器,但是該視圖需要使用 CSRF token 。

解決方案:在 csrf_exempt() 之前使用 requires_csrf_token()(也就是說,requires_csrf_token 裝飾器放在最內(nèi)層)。

只需要保護(hù)視圖的一條路徑

視圖只在一種情況下需要使用 CSRF 防御,而且其他情況不能使用 CSRF 防御。

解決方案:為整個視圖函數(shù)使用 csrf_exempt ,視圖中需要保護(hù)的部分使用 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 實(shí)現(xiàn) POST 請求,并且頁面不包含實(shí)現(xiàn)發(fā)送 CSRF cookie 功能的 csrf_token 表單字段。

解決方案:在發(fā)送視圖中使用 ensure_csrf_cookie() 。

contrib 和可復(fù)用 Apps


開發(fā)過程中可能需要關(guān)閉 CsrfViewMiddleware , contrib 中所有需要 CSRF 防御的視圖都使用 csrf_protect 裝飾器。推薦其他開發(fā)可復(fù)用應(yīng)用的開發(fā)者為需要 CSRF 防御的視圖添加 csrf_protect 裝飾器。

設(shè)置


一些設(shè)置可以控制 Django 的 CSRF 行為:

常見問題


任意發(fā)送的 CSRF token 對( cookie 和 POST 數(shù)據(jù))是一個漏洞嗎?

不是,這是設(shè)計(jì)好的。除了 man-in-the-middle 攻擊 ,攻擊者無法將 CSRF token cookie 發(fā)送到受害者的瀏覽器。因此,成功進(jìn)行攻擊需要通過 XSS 或類似的方式獲得受害者瀏覽器的 cookie ,在這種情況下,攻擊者通常不需要進(jìn)行 CSRF 攻擊。

一些安全檢查工具認(rèn)為這是個安全問題。但如前所述,攻擊者不能竊取用戶瀏覽器的 CSRF cookie 。使用Firebug、Chrome 開發(fā)工具等“竊取”或修改自己的 token 不是一個漏洞。

Django 的 CSRF 防御不會默認(rèn)鏈接到會話(session)是一個問題嗎?

不是,這是設(shè)計(jì)好的。CSRF 防御與會話(session) 不進(jìn)行鏈接將允許我們對允許匿名用戶提交表單的網(wǎng)站(諸如 pastebin 之類)進(jìn)行保護(hù),這種網(wǎng)站的匿名用戶沒有會話(session)。

如果您需要將 CSRF token 存儲在用戶的會話中,請?jiān)O(shè)置 CSRF_USE_SESSIONS。

為什么用戶登錄后還會遇到 CSRF 驗(yàn)證失敗?

出于安全原因,用戶登錄時會輪換 CSRF token 。任何在登錄之前生成表單的頁面都會包含一個舊的、無效的 CSRF token ,我們需要重新加載這些頁面。 如果用戶在登錄后使用后退按鈕,或者他們在不同的瀏覽器標(biāo)簽中進(jìn)行登錄,則可能發(fā)生這種情況。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容