讓我們一起來消滅CSRF跨站請求偽造(下)
寫在前面的話
在本系列文章的上集中,我們跟大家介紹了關于CSRF的一些基本概念,并對常見的幾種CSRF漏洞類型進行了講解。那么接下來,我們就要跟大家討論一下如何才能消滅CSRF。
現代保護機制
實際上,通過修改應用程序源代碼來實現CSRF保護在很多情況下是不現實的,要么就是源代碼無法獲取,要么就是修改應用程序的風險太高了。但我們所設計的解決方案可以輕松地部署到RASP、WAF、反向代理或均衡負載器中,并且可以同時保護一個或多個配置相同的應用程序。
首先我們要知道,正確地使用安全或不安全的HTTP verb是非常重要的。雖然這一點并不能構成一個有效的解決方案,但它是另外兩種方法實現的基礎。在構建應用程序之前,我們需要對其進行架構設計。幸運的是,大多數現代Web框架都有路由的概念,并且可以強制讓節點與HTTP verb配對。在現代框架中,帶有錯誤verb的請求將會導致錯誤的產生。如果你的應用程序中不能實現這種機制的話,請繼續往下看。
另一種方法是驗證請求的發送源,這種方法可以確保發送給應用程序的請求來自于一個受信任的源。在這里,正確使用HTTP verb同樣是非常重要的,如果我們假設只有改變狀態的請求會來自于不安全的請求,那我們就只需要對不安全的請求源進行驗證就可以了。但正如我們之前所討論的,在驗證源的可靠性時我們還會遇到很多的問題。其中的一種解決方案是創建一個安全URL白名單,這樣就可以防止來自外部源的CSRF。
第三種方法,也是最常見的方法,即使用令牌Token。令牌本身有多種形式,但大多數使用的都是同步器令牌(synchronizer token)。說得更加詳細一點,這種令牌主要分為“雙提交令牌”以及“加密令牌”。事實證明,結合使用雙提交令牌以及加密令牌可以提供最好的安全性。
簡單說來,所謂的同步器令牌,就是服務器和瀏覽器之間需要同步一個令牌(唯一的)。安全的請求方法會返回一個令牌,當瀏覽器在發送請求時會攜帶這個令牌,而服務器在處理請求之前,會驗證令牌的有效性。處理完請求之后,服務器還會提供一個新的令牌以保證之前的令牌無法繼續使用(防止重放攻擊)。此時,攻擊者將無法訪問到令牌或者將其插入到惡意請求之中,因為如果攻擊者想這樣做的話,他必須要強迫目標用戶向遠程網站發送請求并訪問請求內容,但SOP可以防止這種情況的發生。這樣一來,攻擊者所能使用的最后一種方法就是利用目標程序可能存在的XSS漏洞了。
需要注意的是,令牌主要有四個部分(一個隨機數,用戶識別符,過期時間以及真實性驗證信息)組成,因此保持其“整體完整性”就非常重要了,其中缺少任何一項都將導致令牌的安全性大打折扣。
在令牌機制的實現過程中,有兩個方面我們需要仔細斟酌,即服務器端和客戶端。其中,服務器端負責生成和驗證令牌,而客戶端負責向需要請求資源的服務器發送令牌。需要注意的是,大家絕對有必要為每一個請求生成一個新的令牌,即使這樣會犧牲一定的性能。除此之外,你也可以在cookie中添加令牌,但你需要確保cookie沒有使用HttpOnly標記。下面這段簡單的示例代碼是生成令牌的常用方法:
- 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
- }
大家可以從上面這段代碼中看到組成令牌的那四個部分。其中,HMAC是用于驗證前三個元素有效性的令牌,并最終會添加到raw的結尾。
- 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
- }
上面這段示例代碼演示的是驗證和計算令牌有效性的常用方法。首先我們需要將令牌拆分成它的四個組成部分,然后第一步就是利用前三個部分生成并驗證HMAC的有效性(與之前的HMAC進行對比)。對比時間一定要確保使用的是固定時間,這樣可以避免基于時間的攻擊。如果驗證成功,我們接下來就要確保令牌沒有過期,最后進行用戶匹配。但在真實場景中,最麻煩的事情就是讓用戶的瀏覽器在發送所有請求時自動提交令牌。
實際上在開發應用的過程中,絕大多數的現代框架都已經幫我們搞定這一切了??蚣軒炜梢蕴幚鞽HR,并將令牌自動插入到請求信息(包括表單)中。但是如果框架沒有幫我們實現的話,我們也可以自己實現這種功能。這一步主要可以分為兩個部分,一個是處理表單提交,另一個是處理XHR。下面這段示例代碼可以處理onclick事件回調:
- 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;
- }
我們可以將這段代碼添加到文檔中,而不是添加到單獨的表單或可點擊的元素之中,因為很有可能表單或元素根本就不存在與頁面DOM之中。我們所指的元素是用戶可以點擊的東西,由于DOM樹的結構以及事件處理系統的不同,所以我們要尋找的是那種可以提交表單的元素,例如input或button標簽。
接下來,我們可以檢測一個標簽是否為input標簽。如果它是,那么我們就可以確保這里有一個提交按鈕了。當我們驗證提交事件已經被觸發之后,我們就可以繼續搜索DOM樹并尋找form標簽了。如果找遍了DOM樹卻沒有找到form標簽,那么就說明元素沒有被提交,除非它使用了XHR。找到form標簽之后,最后一步就是將令牌以一個隱藏input元素添加到表單之中,即創建一個新的元素并將其添加到表單。
- 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);
對于那些并非基于表單的請求,我們就需要想辦法將令牌插入到XHR請求之中了。大多數代碼庫都提供了相關的抽象方法,包括jQuery,但我們需要針對標準XHR API創建我們自己的函數鉤子。通過利用JavaScript的原型繼承機制以及動態特性,我們可以直接將原始的發送方法添加到對象之中,這樣我們就可以隨時調用這些方法了。接下來,我們需要創建一個新的函數并將令牌插入到cookie中,然后再在請求信息中添加一個帶值的header。
不過需要注意的是,對于IE瀏覽器,我們所設計的這種方法只適用于IE 8及其以上版本的IE瀏覽器,因為這些版本才支持方法原型和XHR,雖然IE 支持XHR但并不支持方法原型。具體的瀏覽器支持情況如下圖所示:
總結
在本系列文章中,我們跟大家介紹了關于CSRF的一些基本概念,并對常見的幾種CSRF漏洞類型進行了講解。除此之外,我們還給大家提供了一些用于對付CSRF漏洞的最佳實踐方法。這里我給大家推薦一款名叫Same-Site的擴展插件,它可以幫助我們對cookie進行檢測,并對瀏覽器所發送的cookie進行嚴格的安全限制。這款插件的瀏覽器支持情況如下圖所示: