讓我們一起來消滅CSRF跨站請(qǐng)求偽造(下)
寫在前面的話
在本系列文章的上集中,我們跟大家介紹了關(guān)于CSRF的一些基本概念,并對(duì)常見的幾種CSRF漏洞類型進(jìn)行了講解。那么接下來,我們就要跟大家討論一下如何才能消滅CSRF。
現(xiàn)代保護(hù)機(jī)制
實(shí)際上,通過修改應(yīng)用程序源代碼來實(shí)現(xiàn)CSRF保護(hù)在很多情況下是不現(xiàn)實(shí)的,要么就是源代碼無法獲取,要么就是修改應(yīng)用程序的風(fēng)險(xiǎn)太高了。但我們所設(shè)計(jì)的解決方案可以輕松地部署到RASP、WAF、反向代理或均衡負(fù)載器中,并且可以同時(shí)保護(hù)一個(gè)或多個(gè)配置相同的應(yīng)用程序。
首先我們要知道,正確地使用安全或不安全的HTTP verb是非常重要的。雖然這一點(diǎn)并不能構(gòu)成一個(gè)有效的解決方案,但它是另外兩種方法實(shí)現(xiàn)的基礎(chǔ)。在構(gòu)建應(yīng)用程序之前,我們需要對(duì)其進(jìn)行架構(gòu)設(shè)計(jì)。幸運(yùn)的是,大多數(shù)現(xiàn)代Web框架都有路由的概念,并且可以強(qiáng)制讓節(jié)點(diǎn)與HTTP verb配對(duì)。在現(xiàn)代框架中,帶有錯(cuò)誤verb的請(qǐng)求將會(huì)導(dǎo)致錯(cuò)誤的產(chǎn)生。如果你的應(yīng)用程序中不能實(shí)現(xiàn)這種機(jī)制的話,請(qǐng)繼續(xù)往下看。
另一種方法是驗(yàn)證請(qǐng)求的發(fā)送源,這種方法可以確保發(fā)送給應(yīng)用程序的請(qǐng)求來自于一個(gè)受信任的源。在這里,正確使用HTTP verb同樣是非常重要的,如果我們假設(shè)只有改變狀態(tài)的請(qǐng)求會(huì)來自于不安全的請(qǐng)求,那我們就只需要對(duì)不安全的請(qǐng)求源進(jìn)行驗(yàn)證就可以了。但正如我們之前所討論的,在驗(yàn)證源的可靠性時(shí)我們還會(huì)遇到很多的問題。其中的一種解決方案是創(chuàng)建一個(gè)安全URL白名單,這樣就可以防止來自外部源的CSRF。
第三種方法,也是最常見的方法,即使用令牌Token。令牌本身有多種形式,但大多數(shù)使用的都是同步器令牌(synchronizer token)。說得更加詳細(xì)一點(diǎn),這種令牌主要分為“雙提交令牌”以及“加密令牌”。事實(shí)證明,結(jié)合使用雙提交令牌以及加密令牌可以提供最好的安全性。
簡(jiǎn)單說來,所謂的同步器令牌,就是服務(wù)器和瀏覽器之間需要同步一個(gè)令牌(唯一的)。安全的請(qǐng)求方法會(huì)返回一個(gè)令牌,當(dāng)瀏覽器在發(fā)送請(qǐng)求時(shí)會(huì)攜帶這個(gè)令牌,而服務(wù)器在處理請(qǐng)求之前,會(huì)驗(yàn)證令牌的有效性。處理完請(qǐng)求之后,服務(wù)器還會(huì)提供一個(gè)新的令牌以保證之前的令牌無法繼續(xù)使用(防止重放攻擊)。此時(shí),攻擊者將無法訪問到令牌或者將其插入到惡意請(qǐng)求之中,因?yàn)槿绻粽呦脒@樣做的話,他必須要強(qiáng)迫目標(biāo)用戶向遠(yuǎn)程網(wǎng)站發(fā)送請(qǐng)求并訪問請(qǐng)求內(nèi)容,但SOP可以防止這種情況的發(fā)生。這樣一來,攻擊者所能使用的最后一種方法就是利用目標(biāo)程序可能存在的XSS漏洞了。
需要注意的是,令牌主要有四個(gè)部分(一個(gè)隨機(jī)數(shù),用戶識(shí)別符,過期時(shí)間以及真實(shí)性驗(yàn)證信息)組成,因此保持其“整體完整性”就非常重要了,其中缺少任何一項(xiàng)都將導(dǎo)致令牌的安全性大打折扣。
在令牌機(jī)制的實(shí)現(xiàn)過程中,有兩個(gè)方面我們需要仔細(xì)斟酌,即服務(wù)器端和客戶端。其中,服務(wù)器端負(fù)責(zé)生成和驗(yàn)證令牌,而客戶端負(fù)責(zé)向需要請(qǐng)求資源的服務(wù)器發(fā)送令牌。需要注意的是,大家絕對(duì)有必要為每一個(gè)請(qǐng)求生成一個(gè)新的令牌,即使這樣會(huì)犧牲一定的性能。除此之外,你也可以在cookie中添加令牌,但你需要確保cookie沒有使用HttpOnly標(biāo)記。下面這段簡(jiǎn)單的示例代碼是生成令牌的常用方法:
- String generateToken(int userId, int key) {
- byte[16] data = random()
- expires = time() + 3600
- raw = hex(data) + "-" + userId + "-" + expires
- signature = hmac(sha256, raw, key)
- return raw + "-" + signature
- }
大家可以從上面這段代碼中看到組成令牌的那四個(gè)部分。其中,HMAC是用于驗(yàn)證前三個(gè)元素有效性的令牌,并最終會(huì)添加到raw的結(jié)尾。
- bool validateToken(token, user) {
- parts = token.split("-")
- str = parts[0] + "-" + parts[1] + "-" + parts[2]
- generated = hmac(sha256, str, key)
- if !constantCompare(generated, parts[3]) {
- return false
- }
- if parts[2] < time() {
- return false
- }
- if parts[1] != user {
- return false
- }
- return true
- }
上面這段示例代碼演示的是驗(yàn)證和計(jì)算令牌有效性的常用方法。首先我們需要將令牌拆分成它的四個(gè)組成部分,然后第一步就是利用前三個(gè)部分生成并驗(yàn)證HMAC的有效性(與之前的HMAC進(jìn)行對(duì)比)。對(duì)比時(shí)間一定要確保使用的是固定時(shí)間,這樣可以避免基于時(shí)間的攻擊。如果驗(yàn)證成功,我們接下來就要確保令牌沒有過期,最后進(jìn)行用戶匹配。但在真實(shí)場(chǎng)景中,最麻煩的事情就是讓用戶的瀏覽器在發(fā)送所有請(qǐng)求時(shí)自動(dòng)提交令牌。
實(shí)際上在開發(fā)應(yīng)用的過程中,絕大多數(shù)的現(xiàn)代框架都已經(jīng)幫我們搞定這一切了??蚣軒?kù)可以處理XHR,并將令牌自動(dòng)插入到請(qǐng)求信息(包括表單)中。但是如果框架沒有幫我們實(shí)現(xiàn)的話,我們也可以自己實(shí)現(xiàn)這種功能。這一步主要可以分為兩個(gè)部分,一個(gè)是處理表單提交,另一個(gè)是處理XHR。下面這段示例代碼可以處理onclick事件回調(diào):
- var target = evt.target;
- while (target !== null) {
- if (target.nodeName === 'A' || target.nodeName ===
- 'INPUT' || target.nodeName === 'BUTTON') {
- break;
- }
- targettarget = target.parentNode;
- }
- // We didn't find any of the delegates, bail out
- if (target === null) {
- return;
- }
我們可以將這段代碼添加到文檔中,而不是添加到單獨(dú)的表單或可點(diǎn)擊的元素之中,因?yàn)楹苡锌赡鼙韱位蛟馗揪筒淮嬖谂c頁(yè)面DOM之中。我們所指的元素是用戶可以點(diǎn)擊的東西,由于DOM樹的結(jié)構(gòu)以及事件處理系統(tǒng)的不同,所以我們要尋找的是那種可以提交表單的元素,例如input或button標(biāo)簽。
接下來,我們可以檢測(cè)一個(gè)標(biāo)簽是否為input標(biāo)簽。如果它是,那么我們就可以確保這里有一個(gè)提交按鈕了。當(dāng)我們驗(yàn)證提交事件已經(jīng)被觸發(fā)之后,我們就可以繼續(xù)搜索DOM樹并尋找form標(biāo)簽了。如果找遍了DOM樹卻沒有找到form標(biāo)簽,那么就說明元素沒有被提交,除非它使用了XHR。找到form標(biāo)簽之后,最后一步就是將令牌以一個(gè)隱藏input元素添加到表單之中,即創(chuàng)建一個(gè)新的元素并將其添加到表單。
- var token =
- form.querySelector('input[name="csrf_token"]');
- var tokenValue = getCookieValue('CSRF-TOKEN');
- if (token !== undefined && token !== null) {
- if (token.value !== tokenValue) {
- token.value = tokenValue;
- }
- return;
- }
- var newToken = document.createElement('input');
- newToken.setAttribute('type', 'hidden');
- newToken.setAttribute('name', 'csrf_token');
- newToken.setAttribute('value', tokenValue);
- form.appendChild(newToken);
對(duì)于那些并非基于表單的請(qǐng)求,我們就需要想辦法將令牌插入到XHR請(qǐng)求之中了。大多數(shù)代碼庫(kù)都提供了相關(guān)的抽象方法,包括jQuery,但我們需要針對(duì)標(biāo)準(zhǔn)XHR API創(chuàng)建我們自己的函數(shù)鉤子。通過利用JavaScript的原型繼承機(jī)制以及動(dòng)態(tài)特性,我們可以直接將原始的發(fā)送方法添加到對(duì)象之中,這樣我們就可以隨時(shí)調(diào)用這些方法了。接下來,我們需要?jiǎng)?chuàng)建一個(gè)新的函數(shù)并將令牌插入到cookie中,然后再在請(qǐng)求信息中添加一個(gè)帶值的header。
不過需要注意的是,對(duì)于IE瀏覽器,我們所設(shè)計(jì)的這種方法只適用于IE 8及其以上版本的IE瀏覽器,因?yàn)檫@些版本才支持方法原型和XHR,雖然IE 支持XHR但并不支持方法原型。具體的瀏覽器支持情況如下圖所示:
總結(jié)
在本系列文章中,我們跟大家介紹了關(guān)于CSRF的一些基本概念,并對(duì)常見的幾種CSRF漏洞類型進(jìn)行了講解。除此之外,我們還給大家提供了一些用于對(duì)付CSRF漏洞的最佳實(shí)踐方法。這里我給大家推薦一款名叫Same-Site的擴(kuò)展插件,它可以幫助我們對(duì)cookie進(jìn)行檢測(cè),并對(duì)瀏覽器所發(fā)送的cookie進(jìn)行嚴(yán)格的安全限制。這款插件的瀏覽器支持情況如下圖所示: