(國(guó)防大學(xué)教研保障中心 北京 100091)
CSRF 攻擊是一種常見的Web 攻擊方式,隨著Web 安全技術(shù)的發(fā)展,各類Web 后端框架,如Laravel、Express、Spring Boot、Django等,都內(nèi)置了CSRF 的防御功能。本文以Django 框架為例,分析其CSRF 防御實(shí)現(xiàn)機(jī)制。
CSRF(Cross-site request forgery,跨站請(qǐng)求偽造)是指攻擊者偽裝受信用戶的請(qǐng)求,在受信網(wǎng)站上執(zhí)行被攻擊者非本意操作的攻擊方法。攻擊者使用CSRF 可以利用你的名義發(fā)送郵件消息、盜取賬號(hào)、購買商品、虛擬貨幣轉(zhuǎn)賬等惡意操作,造成個(gè)人隱私泄露以及財(cái)產(chǎn)損失。
http 協(xié)議是無狀態(tài)的,如一個(gè)Web 客戶連續(xù)獲取一個(gè)需要認(rèn)證訪問的Web 服務(wù)器上的信息,可能需要反復(fù)進(jìn)行認(rèn)證。為了解決這個(gè)問題,人們?cè)O(shè)計(jì)了一種得到廣泛應(yīng)用的Cookie 機(jī)制。Cookie 一般由服務(wù)器生成,發(fā)送給瀏覽器,瀏覽器會(huì)將Cookie 的值保存到文本文件中,下次請(qǐng)求同一網(wǎng)站時(shí)就發(fā)送該Cookie 給服務(wù)器,從而實(shí)現(xiàn)保存客戶與服務(wù)器之間的狀態(tài)信息。
Cookie 最典型的應(yīng)用就是判定注冊(cè)用戶是否已經(jīng)登錄,CSRF 就是通過利用已登錄用戶的Cookie 狀態(tài)信息,偽裝成受信任用戶的請(qǐng)求而實(shí)現(xiàn)攻擊的。例如用戶A 登錄了受信任站點(diǎn)B,用戶A 登錄信息驗(yàn)證通過以后,站點(diǎn)B 會(huì)在返回給用戶A 瀏覽器的信息中帶上已登錄的Cookie 信息;用戶A 在未清除登錄站點(diǎn)B 的Cookie 或Cookie未到期情形下,訪問惡意站點(diǎn)C;惡意站點(diǎn)C 的某個(gè)頁面在返回給用戶A 的數(shù)據(jù)中帶有向站點(diǎn)B 發(fā)起的惡意請(qǐng)求,此時(shí)用戶A 會(huì)自動(dòng)向B 站點(diǎn)發(fā)出請(qǐng)求;站點(diǎn)B 根據(jù)用戶A 請(qǐng)求所帶的Cookie,判斷此請(qǐng)求為受信用戶A 所發(fā)送的。至此,惡意站點(diǎn)C 就達(dá)到了偽造用戶A 請(qǐng)求的目的。
(1)驗(yàn)證HTTP Referer
根據(jù)HTTP 協(xié)議,在 HTTP 頭中有一個(gè)字段叫 Referer,它記錄了該 HTTP 請(qǐng)求的來源地址。因此,要防御 CSRF 攻擊,網(wǎng)站可以驗(yàn)證每一個(gè)訪問安全受限頁面請(qǐng)求的 Referer 值,如果是Referer 值的域名與本網(wǎng)站域名相同或在允許的域名之內(nèi),則說明該請(qǐng)求是來自本網(wǎng)站的請(qǐng)求,是合法的,反之,則有可能是CSRF 攻擊,拒絕該請(qǐng)求。
(2)增加隨機(jī)Token
要抵御 CSRF 攻擊,關(guān)鍵在于在請(qǐng)求中放入第三方所不能偽造的信息,且該信息不存在于 Cookie 中,那么可以在 HTTP 請(qǐng)求加入一個(gè)隨機(jī)產(chǎn)生的 Token,并在服務(wù)器端建立一個(gè)攔截器來驗(yàn)證這個(gè)Token,以此判定這個(gè)請(qǐng)求是否偽造。Token 的值必須是隨機(jī)的,可以放入HTTP 請(qǐng)求參數(shù)中或HTTP 請(qǐng)求頭中。
(3)加入驗(yàn)證碼
在轉(zhuǎn)賬、刪除、授權(quán)等一些敏感操作的請(qǐng)求中,加入支付密碼或者校驗(yàn)碼等各類人工驗(yàn)證方法,能很好地遏制CSRF 攻擊。
Django 框架是一個(gè)基于Python 的開源Web 開發(fā)框架,它對(duì)常用Web 開發(fā)模式進(jìn)行了高度封裝。Django 框架在安全方面,為了解決Cookie 存儲(chǔ)數(shù)據(jù)不安全的問題的,Django 框架引入session,用戶瀏覽器只需在Cookie 存儲(chǔ)一個(gè)sessionid,具體的數(shù)據(jù)則通過加密默認(rèn)保存在服務(wù)端session 中,同時(shí)也集成了對(duì)XXS、SQL 注入、CSRF等Web 常見攻擊的防范組件。為實(shí)現(xiàn)CSRF 的防御功能,Django 主要是通過django.middleware.csrf.CsrfViewMiddleware 中間件來實(shí)現(xiàn)。
Django 中間件是一個(gè)輕量的、框架級(jí)別的插件系統(tǒng),用來處理Django 的請(qǐng)求和響應(yīng),在全局范圍內(nèi)改變Django 的輸入和輸出[1]。中間件本質(zhì)上就是一個(gè)自定義類,負(fù)責(zé)實(shí)現(xiàn)一些特定的功能,類中定義了固定的五個(gè)方法:process_request、process_view、process_exception、process_template_responseproces、process_response,Django 會(huì)在瀏覽器請(qǐng)求的特定過程中執(zhí)行這些方法。具體執(zhí)行過程如圖1[2]。
Django 框架中設(shè)置CSRF 功能可分為全局和局部。全局設(shè)置通過在項(xiàng)目配置文件settings.py 的MIDDLEWARE 參數(shù)中添加刪除中間件django.middleware.csrf.CsrfViewMiddleware 來開啟或者關(guān)閉CSRF 保護(hù)。局部設(shè)置通過@csrf_protect、@csrf_exempt 內(nèi)置裝飾器為視圖函數(shù)強(qiáng)制設(shè)置或關(guān)閉CSRF 功能,即便settings.py 中沒有設(shè)置全局中間件。同時(shí)還需要在前端模板的 Form 表單中加入{%csrf_token %}標(biāo)簽,如果通過AJAX 進(jìn)行提交數(shù)據(jù),則把csrftoken 放入請(qǐng)求頭中提交。
從源碼分析,CsrfViewMiddleware 中間件下共定義了7 個(gè)方法,其中定義了中間件的process_request、process_view、process_response三個(gè)固有方法以及_accept、_reject、_get_token、_set_token 四個(gè)私有方法。下面簡(jiǎn)要分析下三個(gè)固有方法的主要作用。
圖1 具體執(zhí)行過程
(1)process_request 方法
process_request 方法在Django 接收到http 請(qǐng)求之后,URL 匹配之前調(diào)用。通過_get_token 方法,從request 對(duì)象的Cookie 或session中獲取csrf_token;如果獲取的csrf_token 不為空時(shí),將其賦值給request.META["CSRF_COOKIE"]。
(2)process_view 方法
process_view 方法在Django 執(zhí)行URL 匹配之后,執(zhí)行視圖函數(shù)之前調(diào)用。首先判讀視圖函數(shù)是否被裝飾器csrf_exempt 裝飾,如果是則直接執(zhí)行視圖渲染模板。接著判斷request 請(qǐng)求方法是否是GET、HEAD、OPTIONS、TRAC,如果不是以上方法,把request.META["CSRF_COOKIE"]賦值給csrf_token,如果csrf_token為None,則執(zhí)行_reject 方法返回403 錯(cuò)誤頁面;如果csrf_token 不None,且為POST 方法時(shí),則從Form 表單中的csrfmiddlewaretoken字段或 request.META 中的 X-CSRFToken 字段取值并賦值給request_csrf_token。最后通過特定算法對(duì) csrf_token 和request_csrf_token 這兩個(gè)值對(duì)比,相等就執(zhí)行視圖渲染模板,反之直接返回403 錯(cuò)誤頁面。
(3)process_response 方法
process_response 方法在所有響應(yīng)返回給瀏覽器之前調(diào)用。通過_set_token 方法設(shè)置 Cookie 或 session 的 csrf_token 值為request.META[“CSRF_COOKIE”],返回response,客戶端瀏覽器完成渲染。
從上面CsrfViewMiddleware 中間件功能分析來看,沒有發(fā)現(xiàn)csrf_token 如何初始生成的,其實(shí)初始token 是在視圖渲染階段,通過django.middleware.csrf 模塊的get_token(與私有_get_token 方法不同)函數(shù)生成的。
在process_request、process_view 方法正常執(zhí)行完之后,就會(huì)執(zhí)行視圖函數(shù)或者視圖類渲染模板。如果在模板中添加了{(lán)%csrf_token%}標(biāo)簽,那么模板在render 函數(shù)渲染過程中,會(huì)在context 字典參數(shù)中加入context['csrf_input']和context['csrf_token'],返回瀏覽器時(shí)會(huì)把{%csrf_token%}替換成Form 表單元素:,其中csrfmiddlewaretoken 值通過get_token 函數(shù)生成。如果表單中沒有{%csrf_token%},渲染的時(shí)候context['csrf_token']不會(huì)被填充,從而不觸發(fā)get_token 方法,也就不會(huì)產(chǎn)生Cookie 信息。
下面分析 get_token 函數(shù)生成 token 的過程:如果request.META["CSRF_COOKIE"]存在,則取其值,使用_unsalt_cipher_token 函數(shù)解析出32 個(gè)字符的csrf_secret 密鑰;如果request.META["CSRF_COOKIE"]不存在,則使用_get_new_csrf_string函數(shù)重新生成一個(gè) 32 個(gè)字符的 csrf_secret 密鑰,再調(diào)用_salt_cipher_secret 生成一個(gè) 64 個(gè)字符的字符串賦給request.META[“CSRF_COOKIE”]。兩種情況判斷得到的csrf_secret 密鑰,最后通過_salt_cipher_secret 函數(shù)加密返回。需要注意的是_salt_cipher_secret(csrf_secret)每次的返回值都不一樣,但csrf_secret值可以保持不變,也就是說每次刷新頁面時(shí),表單csrfmiddlewaretoken 值是隨機(jī)的,瀏覽器Cookie 值保持不變,可見csrf_secret==_unsalt_cipher_token(_salt_cipher_secret(csrf_secret))。
為更清晰的分析CRSF 防御過程,不考慮CsrfViewMiddleware 中間件與Django 其他中間件之間的執(zhí)行流程,只分析該中間件的運(yùn)行機(jī)制。下面我們以用戶提交表單為例進(jìn)行過程分析。
(1)Django 用戶正常訪問過程分析
Django 開啟了CRSF 防御,頁面表單嵌入{%csrf_token%},當(dāng)用戶首次打開表單頁面(沒有該網(wǎng)站的Cookie 數(shù)據(jù)),向服務(wù)器發(fā)出GET 請(qǐng)求,Django 在接收到請(qǐng)求之后,第一步分發(fā)到CsrfViewMiddleware 中間件,執(zhí)行process_request 方法,此時(shí)獲取Cookie 信息為空,則進(jìn)行URL 匹配;第二步URL 匹配后執(zhí)行process_view 方法,因請(qǐng)求方法是GET,則直接執(zhí)行視圖函數(shù);第三步視圖函數(shù)根據(jù)GET 請(qǐng)求方法,執(zhí)行相應(yīng)的render 函數(shù)渲染模板,系統(tǒng)檢查到表單嵌入{%csrf_token%},且request.META[“CSRF_COOKIE”]為空,則執(zhí)行g(shù)et_token 函數(shù),生成一個(gè)新的csrf_secret,csrf_secret 利用_salt_cipher_secret 函數(shù)加密返回,同時(shí)把該值賦值給request.META[“CSRF_COOKIE”]和新生成的csrfmiddlewaretoken input 標(biāo)簽。第四步模板渲染完之后會(huì)觸發(fā)process_response,通過_set_token方法把request.META[“CSRF_COOKIE”]等一些其他Cookie 信息存入用戶Cookie 中,把渲染后的模板頁面返回給用戶瀏覽器。這時(shí)用戶瀏覽器中就顯示出表單頁面,同時(shí)表單中含有csrfmiddlewaretoken input標(biāo)簽,Cookie 中包含csrf_token 數(shù)據(jù)。至此一次完整的用戶GET 請(qǐng)求結(jié)束。
用戶填入表單數(shù)據(jù),向?yàn)g覽器發(fā)出POST 請(qǐng)求。第一步執(zhí)行pro cess_request 方法,通過_get_token 方法獲取Cookie 信息賦值給requ est.META[“CSRF_COOKIE”],經(jīng)URL 匹配后,第二步執(zhí)行process_view 方法,因請(qǐng)求為POST,且request.META["CSRF_COOKIE"]不為空,則分別從Cookie(request.META["CSRF_COOKIE"])和表單中csrfmiddlewaretoken 字段中取值,進(jìn)行算法比較結(jié)果相同,則進(jìn)入第三步執(zhí)行視圖函數(shù),判斷請(qǐng)求方法為POST,執(zhí)行相應(yīng)的處理。第四步把request.META[“CSRF_COOKIE”]值等一些其他Cookie 信息再次存入用戶Cookie 中(Cookie 的值不變,但expire 信息會(huì)有變化),把渲染后的模板頁面返回給用戶瀏覽器。至此用戶POST 請(qǐng)求過程執(zhí)行完畢。
(2)Django 防御CSRF 攻擊過程分析
攻擊者在惡意網(wǎng)站中設(shè)置相同的表單字段,利用已登錄用戶的Cookie 狀態(tài)信息(此時(shí)Cookie 中有數(shù)據(jù)),向授信網(wǎng)站發(fā)出POST 請(qǐng)求,當(dāng)請(qǐng)求執(zhí)行CSRF 中間件的process_view 方法時(shí),因攻擊請(qǐng)求的表單字段中不包含csrfmiddlewaretoken 字段,則直接返回403 錯(cuò)誤頁面。
Django 設(shè)計(jì)CSRF 防御方法中,csrfmiddlewaretoken 字段的值是隨機(jī)變化的,且采用了加密算法,攻擊者很難模擬csrfmiddlewaretoken 的值。為了用戶更加安全,防止Cookie 信息泄露,Django 的Cookie 信息加密放入session 中。
目前主流Web 框架都已加入CSRF 防御功能,且配置過程簡(jiǎn)單,但大多數(shù)人對(duì)框架CSRF 防御機(jī)制了解不多,通過本文對(duì)Django 框架的CSRF 防御原理和實(shí)現(xiàn)機(jī)制的分析,希望給大家對(duì)CSRF 攻擊的原理和Django 框架CSRF 防御架構(gòu)設(shè)計(jì)提供一些參考,同時(shí)進(jìn)一步增強(qiáng)大家的Web 安全防范意識(shí)。