最近在維護一些老專案,除錯時發現請求屢屢被拒絕,仔細看了一下專案的原始碼,發現有csrf token校驗,借這個機會把csrf攻擊學習了一下,總結成文。本文主要總結什麼是csrf攻擊以及有哪些方法來防範,接下來會再寫一篇文章,從原始碼中來學習一下實戰中是如何防禦csrf攻擊的。
主要內容如下:
1. 什麼是CSRF攻擊
CSRF(Cross-site request forgery)跨站請求偽造:攻擊者誘導受害者進入第三方網站,在第三方網站中,向被攻擊網站傳送跨站請求。利用受害者在被攻擊網站已經獲取的註冊憑證(比如cookie),繞過後臺的使用者驗證,達到冒充使用者對被攻擊的網站執行某項操作的目的。
一個典型的CSRF攻擊有著如下的流程:
- 受害者登入a.com,並保留了登入憑證(cookie);
- 攻擊者引誘受害者訪問了b.com;
- b.com向a.com傳送了一個請求:a.com/act=xx。瀏覽器會預設攜帶a.com的cookie;
- a.com接收到請求後,對請求進行驗證,並確認是受害者的憑證,會誤以為是受害者自己傳送的請求;
- a.com以受害者的名義執行act=xx;
- 攻擊完成,攻擊者在受害者不知情的情況下,冒充受害者,讓a.com執行了自己定義的操作;
2. 幾種常見的攻擊型別
2.1 GET型別的CSRF
GET型別的CSRF利用非常簡單,只需要一個HTTP,一般會這樣利用:
<img src="http://bank.example/withdraw?account&=xx&amount=100&to=hacker" >
在受害者訪問含有這個img的頁面之後,瀏覽器會自動向http://bank.example/withdraw?account=xx&amount=100&to=hacker傳送一次HTTP請求。bank.example就會收到包含受害者登入資訊的一次跨域請求。
2.2 Post型別的CSRF
這種型別的CSRF利用起來通常使用的是一個自動提交的表單,如:
<form action = "https://bank.example/withdraw" method="POST"> <input type="hidden" name="account" value="xx" /> <input type="hidden" name="ammount" value="100" /> <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script>
訪問該頁面後,表單會自動提交,相當於模擬使用者完成了一次POST操作。
POST型別的攻擊通常比GET要求更加嚴格一點,但並不複雜。任何個人網站、部落格,被黑客上傳頁面的網站都有可能是發起攻擊的來源,後端介面不能將安全寄託在僅允許POST上面。
2.3 連結型別的CSRF
連結型別的CSRF並不常見,比起其他兩種使用者開啟頁面就會中招的情況,這種需要使用者點選連結才會觸發。這種型別通常是在論壇中釋出圖片中嵌入惡意連結,或者以廣告的形式誘導使用者中招,攻擊者通常會以比較誇張的詞語誘騙使用者點選,例如:
<a href="https://bank.example.com/csrf/withdraw.php?amount=100&to=hacker" target="_blank">
文章和馬伊琍離婚!!!
</a>
由於之前使用者已經登入,在登入狀態還未過期時,只要使用者主動訪問上面的這個PHP頁面,則攻擊成功。
3. CSRF的特點
- 攻擊一般發起在第三方網站,而不是被攻擊的網站。被攻擊的網站無法防止攻擊發生。
- 攻擊利用受害者在被攻擊網站的登入憑證,冒充受害者提交操作;而不是直接竊取資料。
- 整個過程攻擊者並不能獲取到受害者的登入憑證,僅僅是“冒用”。
- 跨站請求可以用各種方式:圖片URL、超連結、CORS、Form提交等等。部分請求方式可以直接嵌入在第三方論壇、文章中,難以進行追蹤。
CSRF通常是跨域的,因為外域通常更容易被攻擊者掌控。但是如果本域下有容易被利用的功能,比如可以發圖和連結的論壇和評論區,攻擊可以直接在本域下進行,而且這種攻擊更加危險。
4. 防護策略
CSRF通常從第三方網站發起,被攻擊的網站無法防止攻擊發生,只能通過增強自己網站針對CSRF的防護能力來提升安全性。上文中講了CSRF的兩個特點:
- CSRF(通常)發生在第三方域名;
- CSRF攻擊者不能獲取到Cookie等資訊,只是使用;
針對這兩點,有如下常用的防護策略,下面會有詳細說明:
- 同源檢測
- CSRF Token
- 雙重Cookie驗證
4.1 同源檢測
這屬於在提交時要求附加本域才能獲取的資訊的方式,這是源於CSRF大多來自第三方網站,通過直接禁止外域(或者不受信任的域名)對我們發起請求的方式來防護攻擊,那又是如何來實現這種方式呢?
在HTTP協議中,有一個Header叫Referer,用於標記來源域名。在瀏覽器發起請求時,大多數情況會自動帶上這個Header,並且不能由前端自定義其內容,所以伺服器可以通過解析這個Header中的域名,確定請求的來源域。對於Ajax請求,圖片和script等資源請求,Referer為發起請求的頁面地址。對於頁面跳轉,Referer為開啟頁面歷史記錄的前一個頁面地址。因此通過Referer中連結的Origin部分就可以得知請求的來源域名。
這種方法並非萬無一失,Referer的值是由瀏覽器提供的,雖然HTTP協議上有明確的要求,但是每個瀏覽器對於Referer的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不是很安全。在部分情況下,攻擊者可以隱藏,甚至修改自己請求的Referer。
前面說過,CSRF大多數情況下來自第三方域名,但並不能排除本域發起。如果攻擊者有許可權在本域釋出評論(含連結、圖片等,統稱UGC),那麼它可以直接在本域發起攻擊,這種情況下同源策略無法達到防護的作用。
總結
同源驗證是一個相對簡單的防範方法,能夠防範絕大多數的CSRF攻擊。但這並不是萬無一失的,對於安全性要求較高,或者有較多使用者輸入內容的網站,我們就要對關鍵的介面做額外的防護措施。
4.2 CSRF Token校驗
CSRF的另一個特徵是,攻擊者無法直接竊取到使用者的資訊(Cookie,Header,網站內容等),僅僅是冒用Cookie中的資訊。而CSRF攻擊之所以能夠成功,是因為伺服器誤把攻擊者傳送的請求當成了使用者自己的請求。那麼我們可以要求所有的使用者請求都攜帶一個CSRF攻擊者無法獲取到的Token。伺服器通過校驗請求是否攜帶正確的Token來把正常的請求和攻擊的請求區分開,這也可以防範CSRF的攻擊。
基於CSRF Token的防護策略大致分為三個步驟:
1.將CSRF Token輸出到頁面中
首先,使用者開啟頁面的時候,伺服器需要給這個使用者生成一個Token,該Token通過加密演算法對資料進行加密,一般Token都包括隨機字串和時間戳的組合,顯然在提交時Token不能再放在Cookie中了,否則又會被攻擊者冒用。因此,為了安全起見Token最好還是存在伺服器的Session中,之後在每次頁面載入時,使用JS遍歷整個DOM樹,對於DOM中所有的a和form標籤後加入Token。這樣可以解決大部分的請求,但是對於在頁面載入之後動態生成的HTML程式碼,這種方法就沒有作用,還需要程式設計師在編碼時手動新增Token。
2.頁面提交的請求攜帶這個Token
對於GET請求,Token將附在請求地址之後,這樣URL就變成 http://url?csrftoken=tokenvalue。而對於 POST請求來說,要在form的最後加上:
<input type="hidden" name="csrftoken" value="tokenvalue"/>
3.伺服器驗證Token是否正確
當使用者從客戶端得到了Token,再次提交給伺服器的時候,伺服器需要判斷Token的有效性,驗證過程是先解密Token,對比加密字串以及時間戳,如果加密字串一致且時間未過期,那麼這個Token就是有效的。
這種方法要比之前檢查Referer或者Origin要安全一些,Token可以在產生並放於Session之中,然後在每次請求時把Token從Session中拿出,與請求中的Token進行比對,但這種方法的比較麻煩的在於如何把Token以引數的形式加入請求。 下面將以Java為例,介紹一些CSRF Token的服務端校驗邏輯,程式碼如下:
HttpServletRequest req = (HttpServletRequest)request; HttpSession s = req.getSession(); // 從sesion中得到csrftoken屬性 String sToken = (String)s.getAttribute("csrftoken"); if(sToken == null){ // 產生新的token放入session中 sToken = generateToken(); s.setAttribute("csrftoken",sToken); chain.doFilter(request,response); }else{ // 從HTTP頭中取得csrftoken String xhrToken = req.getHeader("csrftoken"); // 從請求引數中取得csrftoken String pToken = req.getParameter("csrftoken"); if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ chain.doFilter(request,response); }else if(sToken != null & pToken != null && sToken.equals(pToken)){ chain.doFilter(request,response); }else{ request.getRequestDispatcher("error.jsp").forward(request,response); } }
總結
Token是一個比較有效的CSRF防護方法,只要頁面沒有XSS漏洞洩露Token,那麼介面的CSRF攻擊就無法成功。但是此方法的實現比較複雜,需要給每一個頁面都寫入Token(前端無法使用純靜態頁面),每一個Form及Ajax請求都攜帶這個Token,後端對每一個介面都進行校驗,並保證頁面Token及請求Token一致。這就使得這個防護策略不能在通用的攔截上統一攔截處理,而需要每一個頁面和介面都新增對應的輸出和校驗。這種方法工作量巨大,且有可能遺漏。
4.3 雙重Cookie驗證
在會話中儲存CSRF Token比較繁瑣,而且不能在通用的攔截上統一處理所有的介面。
那麼另一種防禦措施是使用雙重提交Cookie。利用CSRF攻擊不能獲取到使用者Cookie的特點,我們可以要求Ajax和表單請求攜帶一個Cookie中的值。
雙重Cookie採用以下流程:
- 在使用者訪問網站頁面時,向請求域名注入一個Cookie,內容為隨機字串(例如csrfcookie=v8g9e4ksfhw)。
- 在前端向後端發起請求時,取出Cookie,並新增到URL的引數中(接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。
- 後端介面驗證Cookie中的欄位與URL引數中的欄位是否一致,不一致則拒絕。
此方法相對於CSRF Token就簡單了許多。可以直接通過前後端攔截的的方法自動化實現。後端校驗也更加方便,只需進行請求中欄位的對比,而不需要再進行查詢和儲存Token。
當然,此方法並沒有大規模應用,其在大型網站上的安全性還是沒有CSRF Token高,原因我們舉例進行說明。
由於任何跨域都會導致前端無法獲取Cookie中的欄位(包括子域名之間),於是發生瞭如下情況:
- 如果使用者訪問的網站為www.a.com,而後端的api域名為api.a.com。那麼在www.a.com下,前端拿不到api.a.com的Cookie,也就無法完成雙重Cookie認證。
- 於是這個認證Cookie必須被種在a.com下,這樣每個子域都可以訪問。
- 任何一個子域都可以修改a.com下的Cookie。
- 某個子域名存在漏洞被XSS攻擊(例如upload.a.com)。雖然這個子域下並沒有什麼值得竊取的資訊。但攻擊者修改了a.com下的Cookie。
- 攻擊者可以直接使用自己配置的Cookie,對XSS中招的使用者再向www.a.com下,發起CSRF攻擊。
總結
使用雙重Cookie防禦CSRF的優點:
- 無需使用Session,適用面更廣,易於實施。
- Token儲存於客戶端中,不會給伺服器帶來壓力。
- 相對於Token,實施成本更低,可以在前後端統一攔截校驗,而不需要一個個介面和頁面新增。
缺點:
- Cookie中增加了額外的欄位。
- 如果有其他漏洞(例如XSS),攻擊者可以注入Cookie,那麼該防禦方式失效。
- 難以做到子域名的隔離。
- 為了確保Cookie傳輸安全,採用這種防禦方式的最好確保用整站HTTPS的方式,如果還沒切HTTPS的使用這種方式也會有風險。
5. 總結
本文簡單總結了一下CSRF的防護策略:
- CSRF自動防禦策略:同源檢測(Origin 和 Referer 驗證);
- CSRF主動防禦措施:Token驗證 或者 雙重Cookie驗證;
除此之外,保證頁面的冪等性,後端介面不要在GET頁面中做使用者操作。為了更好的防禦CSRF,最佳實踐應該是結合上面總結的防禦措施方式中的優缺點來綜合考慮,結合當前Web應用程式自身的情況做合適的選擇,才能更好的預防CSRF的發生。
參考文獻: