本文是 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 防御:
-
(settings.py中的) Middleware 設(shè)置默認(rèn)激活 CSRF 中間件。如果重寫這個設(shè)置則需要將 ‘django.middleware.csrf.CsrfViewMiddleware' 放在任何視圖處理中間件之前,以確保 CSRF 防御正常工作。
如果你禁用了這個中間件(不推薦這樣做),可以為需要保護(hù)的視圖使用 csrf_protect() 裝飾器。
對于任何指向內(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)。
-
在對應(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 防御基于以下條件:
-
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ī)秘鑰。
為了安全起見,每次用戶登錄都會更改密碼。
-
所有 POST 請求表單都包含一個名為 csrfmiddlewaretoken 的隱藏字段,這個字段的值也是使用秘鑰進(jìn)行加密和解密的密碼。每次調(diào)用 get_token() 都會重新生成秘鑰,從而保證每次響應(yīng)的表單字段值都會發(fā)生變化。
這一部分由模板標(biāo)簽完成。
-
除了 GET, HEAD, OPTIONS 或 TRACE 請求,其它請求必須設(shè)置 CSRF cookie ,并且必須設(shè)置 csrfmiddlewaretoken 字段而且必須正確。否則,用戶將看到 403 錯誤。
驗(yàn)證 csrfmiddlewaretoken 字段的值時,只對 token 和 cookie 中的密碼進(jìn)行比較。驗(yàn)證允許每次使用不同的token 。每個請求可以使用自己的 token 。
這項(xiàng)檢查由 CsrfViewMiddleware 完成。
-
另外,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ā)生這種情況。