CSRF攻防

CSRF 背景與介紹

CSRF定義: 跨站請求僞造(英語:Cross-site request forgery),也被稱爲 one-click attack 或者 session riding,通常縮寫爲 CSRF 或者 XSRF, 是一種挾制用戶在當前已登錄的Web應用程序上執行非本意的操作的攻擊方法。

簡單地說,是攻擊者通過一些技術手段欺騙用戶的瀏覽器去訪問一個自己曾經認證過的網站並執行一些操作(如發郵件,發消息,甚至財產操作如轉賬和購買商品)。由於瀏覽器曾經認證過,所以被訪問的網站會認爲是真正的用戶操作而去執行。這利用了web中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個用戶的瀏覽器,卻不能保證請求本身是用戶自願發出的。

CSRF地位:是一種網絡攻擊方式,是互聯網重大安全隱患之一,NYTimes.com(紐約時報)、Metafilter,YouTube、Gmail和百度HI都受到過此類攻擊。

對比XSS:跟跨網站腳本(XSS)相比,XSS 利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。

CSRF 攻擊實例

daguanren(大官人)在銀行有一筆存款,輸入用戶名密碼登錄銀行網銀後發送請求進行個人名下賬戶轉賬 :

http://www.bank.example/withdraw?account=daguanren1&amount=999&for=daguanren2

將daguanren1中的999塊轉到了daguanren2賬號中。通常用戶登錄後,系統會保存用戶登錄的session值(可能是用戶手機號、賬號等)。但如果這時daguanren不小心新開一個tab頁面進入了一個黑客jinlian(金蓮)的網站,而金蓮網站的頁面中嵌有如下html標籤:

<!DOCTYPE html>
<html>
    <!--其他頁面元素-->

    <img src=http://www.bank.example/withdraw?account=daguanren1&amount=888&for=jinlian width='0' height='0'>

    <!--其他頁面元素-->
</html>

這個請求就會附帶上daguanren的session值,成功將大官人的888元轉至jinlian的賬戶上。但如果daguanren之前沒有登錄網銀,而是直接打開jinlian的網站,則由於沒有session值,不會被攻擊。以上示例雖然是get請求,post請求提交的表單同樣會被攻擊。

<iframe style="display:none" name="csrf-frame"></iframe>
<form method='POST' action='http://www.bank.example/withdraw' target="csrf-frame" id="csrf-form">
  <input type='hidden' name='account' value='daguanren1'>
  <input type='hidden' name='amount' value='888'>
  <input type='hidden' name='for' value='jinlian'>
  <input type='submit' value='submit'>
</form>
<script>document.getElementById("csrf-form").submit()</script>

CSRF

所以要被CSRF攻擊,必須同時滿足兩個條件:

  1. 登錄受信任網站A,並在本地生成Cookie。
  2. 在不登出A的情況下,訪問危險網站B。

CSRF 攻擊的對象

在討論如何抵禦 CSRF 之前,先要明確 CSRF 攻擊的對象,也就是要保護的對象。從以上的例子可知,CSRF 攻擊是黑客藉助受害者的 cookie(session) 騙取服務器的信任,但是黑客並不能拿到 cookie,也看不到 cookie 的內容。另外,對於服務器返回的結果,由於瀏覽器同源策略的限制,黑客也無法進行解析。因此,黑客無法從返回的結果中得到任何東西,他所能做的就是給服務器發送請求,以執行請求中所描述的命令,在服務器端直接改變數據的值,而非竊取服務器中的數據。所以,我們要保護的對象是那些可以直接產生數據改變的服務,而對於讀取數據的服務,則不需要進行 CSRF 的保護。比如銀行系統中轉賬的請求會直接改變賬戶的金額,會遭到 CSRF 攻擊,需要保護。而查詢餘額是對金額的讀取操作,不會改變數據,CSRF 攻擊無法解析服務器返回的結果,無需保護。

故:增刪改需要防範CSRF攻擊,而讀無需防範。

當前防禦 CSRF 的幾種策略

在業界目前防禦 CSRF 攻擊主要有四種策略:

  1. 驗證 HTTP Referer 字段;
  2. 在請求地址中添加 token 並驗證;
  3. 在 HTTP 頭中自定義屬性並驗證;
  4. Chrome瀏覽器端啓用SameSite cookie

1、驗證 HTTP Referer 字段

什麼是HTTP Referer?下面GIF圖是由百度跳轉到QQ郵箱頁面的Referer查看示意:referer.gif

可以看出Referer爲

Referer:https://www.baidu.com/

根據 HTTP 協議,在 HTTP 頭(request 的 header)中有一個字段叫 Referer,它記錄了該 HTTP 請求的來源地址。如果黑客要對銀行網站實施 CSRF 攻擊,當用戶通過黑客的網站發送請求到銀行時,該請求的 Referer 值是指向黑客的網站而不是用戶的網站。因此,要防禦 CSRF 攻擊,銀行網站只需要對於每一個轉賬請求驗證其 Referer 值,如果是以 www.bank.example開頭的域名,則說明該請求是來自銀行網站自己的請求,是合法的。如果 Referer 是其他網站的話,則有可能是黑客的 CSRF 攻擊,拒絕該請求。

這種方法的顯而易見的好處就是簡單易行,網站的普通開發人員不需要操心 CSRF 的漏洞,只需要在最後給所有安全敏感的請求統一增加一個攔截器來檢查 Referer 的值就可以。特別是對於當前現有的系統,不需要改變當前系統的任何已有代碼和邏輯。

然而,這種方法並非萬無一失。Referer 的值是由瀏覽器提供的,雖然 HTTP 協議上有明確的要求,但是每個瀏覽器對於 Referer 的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不安全。事實上,對於某些瀏覽器,比如 IE6 或 FF2,目前已經有一些方法可以篡改 Referer 值。如果 www.bank.example網站支持 IE6 瀏覽器,黑客完全可以把用戶瀏覽器的 Referer 值設爲以 www.bank.example域名開頭的地址,這樣就可以通過驗證,從而進行 CSRF 攻擊。

即便是使用最新的瀏覽器,黑客無法篡改 Referer 值,這種方法仍然有問題。因爲 Referer 值會記錄下用戶的訪問來源,有些用戶認爲這樣會侵犯到他們自己的隱私權,特別是有些組織擔心 Referer 值會把組織內網中的某些信息泄露到外網中。因此,用戶自己可以設置瀏覽器使其在發送請求時不再提供 Referer。當他們正常訪問銀行網站時,網站會因爲請求沒有 Referer 值而認爲是 CSRF 攻擊,拒絕合法用戶的訪問。

另外,如果Referer的判斷邏輯寫的不嚴密的話,也容易被攻破,例如

const referer = request.headers.referer;
if (referer.indexOf('www.bank.example') > -1) {
  // pass
}

如果黑客的網站是www.bank.example.hack.com,則referer檢查無效。

2、在請求地址中添加 token 並驗證

CSRF 攻擊之所以能夠成功,是因爲黑客可以完全僞造用戶的請求,該請求中所有的用戶驗證信息都是存在於 cookie 中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶的 cookie 來通過安全驗證。要抵禦 CSRF,關鍵在於在請求中放入黑客所不能僞造的信息,並且該信息不存在於 cookie 之中。可以在 HTTP 請求中以參數的形式加入一個隨機產生的 token,並在服務器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內容不正確,則認爲可能是 CSRF 攻擊而拒絕該請求。

這種方法要比檢查 Referer 要安全一些,token 可以在用戶登陸後產生並放於 session 之中,然後在每次請求時把 token 從 session 中拿出,與請求中的 token 進行比對,但這種方法的難點在於如何把 token 以參數的形式加入請求。對於 GET 請求,token 將附在請求地址之後,這樣 URL 就變成

http://url?csrftoken=tokenvalue

而對於 POST 請求來說,要在 form 的最後加上

 <input type="hidden" name="csrftoken" value="tokenvalue"/>

該方法有一個缺點是難以保證 token 本身的安全。特別是在一些論壇之類支持用戶自己發表內容的網站,黑客可以在上面發佈自己個人網站的地址。由於系統也會在這個地址後面加上 token,黑客可以在自己的網站上得到這個 token,並馬上就可以發動 CSRF 攻擊。爲了避免這一點,系統可以在添加 token 的時候增加一個判斷,如果這個鏈接是鏈到自己本站的,就在後面添加 token,如果是通向外網則不加。不過,即使這個 csrftoken 不以參數的形式附加在請求之中,黑客的網站也同樣可以通過 Referer 來得到這個 token 值以發動 CSRF 攻擊。這也是一些用戶喜歡手動關閉瀏覽器 Referer 功能的原因。

3、在 HTTP 頭中自定義屬性並驗證

這種方法也是使用 token 並進行驗證,和上一種方法不同的是,這裏並不是把 token 以參數的形式置於 HTTP 請求之中,而是把它放到 HTTP 頭中自定義的屬性裏。通過 XMLHttpRequest 這個類,可以一次性給所有該類請求加上 csrftoken 這個 HTTP 頭屬性,並把 token 值放入其中。這樣解決了上種方法在請求中加入 token 的不便,同時,通過 XMLHttpRequest 請求的地址不會被記錄到瀏覽器的地址欄,也不用擔心 token 會透過 Referer 泄露到其他網站中去。

然而這種方法的侷限性非常大。XMLHttpRequest 請求通常用於 Ajax 方法中對於頁面局部的異步刷新,並非所有的請求都適合用這個類來發起,而且通過該類請求得到的頁面不能被瀏覽器所記錄下,從而進行前進,後退,刷新,收藏等操作,給用戶帶來不便。另外,對於沒有進行 CSRF 防護的遺留系統來說,要採用這種方法來進行防護,要把所有請求都改爲 XMLHttpRequest 請求,這樣幾乎是要重寫整個網站,這代價無疑是不能接受的。

4、Chrome瀏覽器端啓用SameSite cookie

下面介紹如何啓用SameSite cookie的設置,很簡單。

原本的 Cookie 的 header 設置是長這樣:

Set-Cookie: session_id=esadfas325

需要在尾部增加 SameSite 就好:

Set-Cookie: session_id=esdfas32e5; SameSite

SameSite 有兩種模式,Lax跟Strict模式,默認啓用Strict模式,可以自己指定模式:

Set-Cookie: session_id=esdfas32e5; SameSite=Strict
Set-Cookie: foo=bar; SameSite=Lax

Strict模式規定 cookie 只允許相同的site使用,不應該在任何的 cross site request 被加上去。即a標籤、form表單和XMLHttpRequest提交的內容,只要是提交到不同的site去,就不會帶上cookie。

但也存在不便,例如朋友發送過來我已經登陸過的一個頁面鏈接,我點開後,該頁面仍然需要重新登錄。

有兩種處理辦法,第一種是與Amazon一樣,準備兩組不同的cookie,第一組用於維持登錄狀態不設定SameSite,第二組針對的是一些敏感操作會用到(例如購買、支付、設定賬戶等)嚴格設定SameSite。

基於這個思路,就產生了 SameSite 的另一一種模式:Lax模式。

Lax 模式打開了一些限制,例如

<a>
<link rel="prerender">
<form method="GET">

這些都會帶上cookie。但是 POST 方法 的 form,或是只要是 POST, PUT, DELETE 這些方法,就不會帶cookie。

但一定注意將重要的請求方式改成POST,否則GET仍然會被攻擊。

PS:該方式目前僅Chrome支持。

代碼示例

以下JAVA代碼示例,主要摘自這個文章CSRF 攻擊的應對之道

下文對上述前三種方法分別用代碼進行示例。無論使用何種方法,在服務器端的攔截器必不可少,它將負責檢查到來的請求是否符合要求,然後視結果而決定是否繼續請求或者丟棄。在 Java 中,攔截器是由 Filter 來實現的。我們可以編寫一個 Filter,並在 web.xml 中對其進行配置,使其對於訪問所有需要 CSRF 保護的資源的請求進行攔截。 在 filter 中對請求的 Referer 驗證代碼如下

清單 1. 在 Filter 中驗證 Referer

// 從 HTTP 頭中取得 Referer 值
String referer=request.getHeader("Referer"); 
// 判斷 Referer 是否以 bank.example 開頭
if((referer!=null) &&(referer.trim().startsWith(“bank.example”))){ 
   chain.doFilter(request, response); 
}else{ 
   request.getRequestDispatcher(“error.jsp”).forward(request,response); 
}

以上代碼先取得 Referer 值,然後進行判斷,當其非空並以 bank.example 開頭時,則繼續請求,否則的話可能是 CSRF 攻擊,轉到 error.jsp 頁面。

如果要進一步驗證請求中的 token 值,代碼如下

清單 2. 在 filter 中驗證請求中的 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); 
   } 
}

首先判斷 session 中有沒有 csrftoken,如果沒有,則認爲是第一次訪問,session 是新建立的,這時生成一個新的 token,放於 session 之中,並繼續執行請求。如果 session 中已經有 csrftoken,則說明用戶已經與服務器之間建立了一個活躍的 session,這時要看這個請求中有沒有同時附帶這個 token,由於請求可能來自於常規的訪問或是 XMLHttpRequest 異步訪問,我們分別嘗試從請求中獲取 csrftoken 參數以及從 HTTP 頭中獲取 csrftoken 自定義屬性並與 session 中的值進行比較,只要有一個地方帶有有效 token,就判定請求合法,可以繼續執行,否則就轉到錯誤頁面。生成 token 有很多種方法,任何的隨機算法都可以使用,Java 的 UUID 類也是一個不錯的選擇。

除了在服務器端利用 filter 來驗證 token 的值以外,我們還需要在客戶端給每個請求附加上這個 token,這是利用 js 來給 html 中的鏈接和表單請求地址附加 csrftoken 代碼,其中已定義 token 爲全局變量,其值可以從 session 中得到。

清單 3. 在客戶端對於請求附加 token


function appendToken(){ 
   updateForms(); 
   updateTags(); 
} 

function updateForms() { 
   // 得到頁面中所有的 form 元素
   var forms = document.getElementsByTagName('form'); 
   for(i=0; i<forms.length; i++) { 
       var url = forms[i].action; 

       // 如果這個 form 的 action 值爲空,則不附加 csrftoken 
       if(url == null || url == "" ) continue; 

       // 動態生成 input 元素,加入到 form 之後
       var e = document.createElement("input"); 
       e.name = "csrftoken"; 
       e.value = token; 
       e.type="hidden"; 
       forms[i].appendChild(e); 
   } 
} 

function updateTags() { 
   var all = document.getElementsByTagName('a'); 
   var len = all.length; 

   // 遍歷所有 a 元素
   for(var i=0; i<len; i++) { 
       var e = all[i]; 
       updateTag(e, 'href', token); 
   } 
} 

function updateTag(element, attr, token) { 
   var location = element.getAttribute(attr); 
   if(location != null && location != '' '' ) { 
       var fragmentIndex = location.indexOf('#'); 
       var fragment = null; 
       if(fragmentIndex != -1){ 

           //url 中含有隻相當頁的錨標記
           fragment = location.substring(fragmentIndex); 
           location = location.substring(0,fragmentIndex); 
       } 

       var index = location.indexOf('?'); 

       if(index != -1) { 
           //url 中已含有其他參數
           location = location + '&csrftoken=' + token; 
       } else { 
           //url 中沒有其他參數
           location = location + '?csrftoken=' + token; 
       } 
       if(fragment != null){ 
           location += fragment; 
       } 

       element.setAttribute(attr, location); 
   } 
}

在客戶端 html 中,主要是有兩個地方需要加上 token,一個是表單 form,另一個就是鏈接 a。這段代碼首先遍歷所有的 form,在 form 最後添加一隱藏字段,把 csrftoken 放入其中。然後,代碼遍歷所有的鏈接標記 a,在其 href 屬性中加入 csrftoken 參數。注意對於 a.href 來說,可能該屬性已經有參數,或者有錨標記。因此需要分情況討論,以不同的格式把 csrftoken 加入其中。

如果你的網站使用 XMLHttpRequest,那麼還需要在 HTTP 頭中自定義 csrftoken 屬性,利用 dojo.xhr 給 XMLHttpRequest 加上自定義屬性代碼如下:

清單 4. 在 HTTP 頭中自定義屬性


var plainXhr = dojo.xhr; 

// 重寫 dojo.xhr 方法
dojo.xhr = function(method,args,hasBody) { 
   // 確保 header 對象存在
   args.headers = args.header || {}; 

   tokenValue = '<%=request.getSession(false).getAttribute("csrftoken")%>'; 
   var token = dojo.getObject("tokenValue"); 

   // 把 csrftoken 屬性放到頭中
   args.headers["csrftoken"] = (token) ? token : "  "; 
   return plainXhr(method,args,hasBody); 
};

這裏改寫了 dojo.xhr 的方法,首先確保 dojo.xhr 中存在 HTTP 頭,然後在 args.headers 中添加 csrftoken 字段,並把 token 值從 session 裏拿出放入字段中。

PHP代碼請參考這篇文章淺談CSRF攻擊方式

總結

通過上文分析,目前最便捷的方式是直接判別Referer值,確保同域請求才給放行。如果系統必須支持IE6,那麼就要使用 token 來進行驗證,在大部分情況下,使用 XmlHttpRequest 並不合適,token 只能以參數的形式放於請求之中,若你的系統不支持用戶自己發佈信息,那這種程度的防護已經足夠,否則的話,你仍然難以防範 token 被黑客竊取並發動攻擊。在這種情況下,你需要小心規劃你網站提供的各種服務,從中間找出那些允許用戶自己發佈信息的部分,把它們與其他服務分開,使用不同的 token 進行保護,這樣可以有效抵禦黑客對於你關鍵服務的攻擊,把危害降到最低。

如果是開發一個全新的系統,則抵禦 CSRF 的選擇要大得多。筆者建議對於重要的服務,可以儘量使用 XMLHttpRequest 來訪問,這樣增加 token 要容易很多。另外儘量避免在 js 代碼中使用複雜邏輯來構造常規的同步請求來訪問需要 CSRF 保護的資源,比如 window.location 和 document.createElement(“a”) 之類,這樣也可以減少在附加 token 時產生的不必要的麻煩。

最後,要記住 CSRF 不是黑客唯一的攻擊手段,無論你 CSRF 防範有多麼嚴密,如果你係統有其他安全漏洞,比如跨站域腳本攻擊 XSS,那麼黑客就可以繞過你的安全防護,展開包括 CSRF 在內的各種攻擊,你的防線將如同虛設。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章