看一下我從網上找的原理圖,結合舉例描述,多看一遍你就知道怎麼回事了。
CSRF是什麼呢?CSRF全名是Cross-site request forgery,是一種對網站的惡意利用,CSRF比XSS更具危險性。想要深入理解CSRF的攻擊特性我們有必要了解一下網站session的工作原理。
session我想大家都不陌生,無論你用.net或PHP開發過網站的都肯定用過session物件,然而session它是如何工作的呢?如果你不清楚請往下看。
先問個小問題:如果我把瀏覽器的cookie禁用了,大家認為session還能正常工作嗎?
答案是否定的,我在這邊舉個簡單的例子幫助大家理解session。
比如我買了一張高爾夫俱樂部的會員卡,俱樂部給了我一張帶有卡號的會員卡。我能享受哪些權利(比如我是高階會員卡可以打19洞和後付費喝飲料,而初級會員卡只能在練習場揮杆)以及我的個人資料都是儲存在高爾夫俱樂部的資料庫裡的。我每次去高爾夫俱樂部只需要出示這張高階會員卡,俱樂部就知道我是誰了,並且為我服務了。
這裡我們的高階會員卡卡號 = 儲存在cookie的sessionid;
而我的高階會員卡權利和個人資訊就是服務端的session物件了。
我們知道http請求是無狀態的,也就是說每次http請求都是獨立的無關之前的操作的,但是每次http請求都會將本域下的所有cookie作為http請求頭的一部分傳送給服務端,所以服務端就根據請求中的cookie存放的sessionid去session物件中找到該會員資料了。
當然session的儲存方法多種多樣,可以儲存在檔案中,也可以記憶體裡,考慮到分散式的橫向擴充套件我們還是建議把它儲存在第三方媒介中,比如redis或者mongodb。
我們理解了session的工作機制後,CSRF也就很容易理解了。CSRF攻擊就相當於惡意使用者A複製了我的高階會員卡,哪天惡意使用者A也可以拿著這張假冒的高階會員卡去高爾夫俱樂部打19洞,享受美味的飲料了,而我在月底就會收到高爾夫俱樂部的賬單!
瞭解CSRF的機制之後,危害性我相信大家已經不言而喻了,我可以偽造某一個使用者的身份給其好友傳送垃圾資訊,這些垃圾資訊的超連結可能帶有木馬程式或者一些欺騙資訊(比如借錢之類的),如果CSRF傳送的垃圾資訊還帶有蠕蟲連結的話,那些接收到這些有害資訊的好友萬一開啟私信中的連線就也成為了有害資訊的散播著,這樣數以萬計的使用者被竊取了資料種植了木馬。整個網站的應用就可能在瞬間奔潰,使用者投訴,使用者流失,公司聲譽一落千丈甚至面臨倒閉。曾經在MSN上,一個美國的19歲的小夥子Samy利用css的background漏洞幾小時內讓100多萬使用者成功的感染了他的蠕蟲,雖然這個蠕蟲並沒有破壞整個應用,只是在每一個使用者的簽名後面都增加了一句“Samy 是我的偶像”,但是一旦這些漏洞被惡意使用者利用,後果將不堪設想,同樣的事情也曾經發生在新浪微博上面。
舉例:
CSRF攻擊的主要目的是讓使用者在不知情的情況下攻擊自己已登入的一個系統,類似於釣魚。如使用者當前已經登入了郵箱,或bbs,同時使用者又在使用另外一個,已經被你控制的站點,我們姑且叫它釣魚網站。這個網站上面可能因為某個圖片吸引你,你去點選一下,此時可能就會觸發一個js的點選事件,構造一個bbs發帖的請求,去往你的bbs發帖,由於當前你的瀏覽器狀態已經是登陸狀態,所以session登陸cookie資訊都會跟正常的請求一樣,純天然的利用當前的登陸狀態,讓使用者在不知情的情況下,幫你發帖或幹其他事情。
CSRF的原理
要完成一次CSRF攻擊,受害者必須依次完成兩個步驟:
1.登入受信任網站A,並在本地生成Cookie。
2.在不登出A的情況下,訪問危險網站B。
看到這裡,你也許會說:“如果我不滿足以上兩個條件中的一個,我就不會受到CSRF的攻擊”。是的,確實如此,但你不能保證以下情況不會發生:
1.你不能保證你登入了一個網站後,不再開啟一個tab頁面並訪問另外的網站
2.你不能保證你關閉瀏覽器了後,你本地的Cookie立刻過期,你上次的會話已經結束。(事實上,關閉瀏覽器不能結束一個會話,但大多數人都會錯誤的認為關閉瀏覽器就等於退出登入/結束會話了......)
3.上圖中所謂的攻擊網站,可能是一個存在其他漏洞的可信任的經常被人訪問的網站。
上面大概地講了一下CSRF攻擊的思想,下面我將用幾個例子詳細說說具體的CSRF攻擊,這裡我以一個銀行轉賬的操作作為例子(僅僅是例子,真實的銀行網站沒這麼傻:>)
示例1:
銀行網站A,它以GET請求來完成銀行轉賬的操作,如:
http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危險網站B,它裡面有一段HTML的程式碼如下:
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
首先,你登入了銀行網站A,然後訪問危險網站B,噢,這時你會發現你的銀行賬戶少了1000塊......
為什麼會這樣呢?原因是銀行網站A違反了HTTP規範,使用GET請求更新資源。在訪問危險網站B的之前,你已經登入了銀行網站A,而B中的載入中...以GET的方式請求第三方資源(這裡的第三方就是指銀行網站了,原本這是一個合法的請求,但這裡被不法分子利用了),所以你的瀏覽器會帶上你的銀行網站A的Cookie發出Get請求,去獲取資源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,結果銀行網站伺服器收到請求後,認為這是一個更新資源操作(轉賬操作),所以就立刻進行轉賬操作......
示例2:
為了杜絕上面的問題,銀行決定改用POST請求完成轉賬操作。
銀行網站A的WEB表單如下:
<form action="Transfer.php" method="POST"> <p>ToBankId: <input type="text" name="toBankId" /></p> <p>Money: <input type="text" name="money" /></p> <p><input type="submit" value="Transfer" /></p> </form>
後臺處理頁面Transfer.php如下:
<?php session_start(); if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money'])) { buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']); } ?>
危險網站B,仍然只是包含那句HTML程式碼:
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
和示例1中的操作一樣,你首先登入了銀行網站A,然後訪問危險網站B,結果.....和示例1一樣,你再次沒了1000塊~T_T,這次事故的原因是:銀行後臺使用了$_REQUEST去獲取請求的資料,而$_REQUEST既可以獲取GET請求的資料,也可以獲取POST請求的資料,這就造成了在後臺處理程式無法區分這到底是GET請求的資料還是POST請求的資料。在PHP中,可以使用$_GET和$_POST分別獲取GET請求和POST請求的資料。在JAVA中,用於獲取請求資料request一樣存在不能區分GET請求資料和POST資料的問題。
示例3:
經過前面2個慘痛的教訓,銀行決定把獲取請求資料的方法也改了,改用$_POST,只獲取POST請求的資料,後臺處理頁面Transfer.php程式碼如下:
<?php session_start(); if (isset($_POST['toBankId'] && isset($_POST['money'])) { buy_stocks($_POST['toBankId'], $_POST['money']); } ?>
然而,危險網站B與時俱進,它改了一下程式碼:
<html> <head> <script type="text/javascript"> function steal() { iframe = document.frames["steal"]; iframe.document.Submit("transfer"); } </script> </head> <body onload="steal()"> <iframe name="steal" display="none"> <form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php"> <input type="hidden" name="toBankId" value="11"> <input type="hidden" name="money" value="1000"> </form> </iframe> </body> </html>
如果使用者仍是繼續上面的操作,很不幸,結果將會是再次不見1000塊......因為這裡危險網站B暗地裡傳送了POST請求到銀行!
總結一下上面3個例子,CSRF主要的攻擊模式基本上是以上的3種,其中以第1,2種最為嚴重,因為觸發條件很簡單,一個載入中...就可以了,而第3種比較麻煩,需要使用JavaScript,所以使用的機會會比前面的少很多,但無論是哪種情況,只要觸發了CSRF攻擊,後果都有可能很嚴重。
理解上面的3種攻擊模式,其實可以看出,CSRF攻擊是源於WEB的隱式身份驗證機制!WEB的身份驗證機制雖然可以保證一個請求是來自於某個使用者的瀏覽器,但卻無法保證該請求是使用者批准傳送的!
五.CSRF的防禦
CSRF 的防範機制有很多種,防範的方法也根據 CSRF 攻擊方式的不斷升級而不斷演化。常用的有檢查 Refer 頭部資訊,使用一次性令牌,使用驗證圖片等手段。出於效能的考慮,如果每個請求都加入令牌驗證將極大的增加伺服器的負擔,具體採用那種方法更合理,需要謹慎審視每種保護的優缺點。
1. 檢查 HTTP 頭部 Refer 資訊,這是防止 CSRF 的最簡單容易實現的一種手段。根據 RFC 對於 HTTP 協議裡面 Refer 的定義,Refer 資訊跟隨出現在每個 Http 請求頭部。Server 端在收到請求之後,可以去檢查這個頭資訊,只接受來自本域的請求而忽略外部域的請求,這樣就可以避免了很多風險。當然這種檢查方式由於過於簡單也有它自身的弱點:
a) 首先是檢查 Refer 資訊並不能防範來自本域的攻擊。在企業業務網站上,經常會有同域的論壇,郵件等形式的 Web 應用程式存在,來自這些地方的 CSRF 攻擊所攜帶的就是本域的 Refer 域資訊,因此不能被這種防禦手段所阻止。
b) 同樣,某些直接傳送 HTTP 請求的方式(指非瀏覽器,比如用後臺程式碼等方法)可以偽造一些 Refer 資訊,雖然直接進行頭資訊偽造的方式屬於直接傳送請求,很難跟隨傳送 cookie,但由於目前客戶端手段層出不窮,flash,javascript 等大規模使用,從客戶端進行 refer 的偽造,尤其是在客戶端瀏覽器安裝了越來越多的外掛的情況下已經成為可能了。
2. 使用一次性令牌,這是當前 Web 應用程式的設計人員廣泛使用的一種方式,方法是對於 Get 請求,在 URL 裡面加入一個令牌,對於 Post 請求,在隱藏域中加入一個令牌。這個令牌由 server 端生成,由程式設計人員控制在客戶端傳送請求的時候使請求攜帶本令牌然後在 Server 端進行驗證。但在令牌的設計上目前存在著幾個錯誤的方案:
a) 使用和 Session 獨立的令牌生成方式。這種令牌的值和 Session 無關,因此容易被其他使用者偽造。這裡的其他使用者指的是當前 Web 應用程式的其他使用者和活躍在網路傳輸階段各個設定上的監聽者,這種惡意使用者可能使用自己的令牌來進行替換以便達到偽造的目的。
b) 完全使用 Session 認證資訊作為令牌的生成方式。這種保護方式對於保護 CSRF 是起了作用的,但是可能會造成其他危害,具體來說,如果某些 URL 或者網頁被拷貝下來與其他人共享,那麼這些 URL 或者拷貝下來的網頁中可能會含有使用者的會話資訊,這種資訊一旦被惡意使用者獲得,就能造成極大的危害。
因此,一個正確的令牌設計應該是使用 Session 資訊做 Hash,用得出的雜湊值來做 CSRF 的令牌。
3. 使用驗證圖片,這種方法的出現的作用是對於機器人暴力攻擊的防止。但在 CSRF 的防範上,也有一些安全性要求比較高的的應用程式結合驗證圖片和一次性令牌來做雙重保護。由於這種圖片驗證資訊很難被惡意程式在客戶端識別,因此能夠提高更強的保護。當客戶端的瀏覽器可能已經處於一種不安全的環境中的情況下(比如客戶端的安全級別設定較低,客戶端瀏覽器安裝了不安全的外掛等)。
以上給的這些只是防範 CSRF 的比較通用的一些方法,Web 開發人員可以根據自己對自己的應用程式的功能的理解來確定安全級別的要求從而選擇使用不同的保護措施,也推薦在同一應用程式內部結合使用多種方法來進行保護。
注:加防也會極大的影響效能,就如在高速路上放一個收費站,建議只在重要操作上加防。如位慎重考慮。
PHP的防範措施:
CSRF的防禦可以從服務端和客戶端兩方面著手,防禦效果是從服務端著手效果比較好,現在一般的CSRF防禦也都在服務端進行。
服務端進行CSRF防禦
服務端的CSRF方式方法很多樣,但總的思想都是一致的,就是在客戶端頁面增加偽隨機數。
(1).Cookie Hashing(所有表單都包含同一個偽隨機值):
這可能是最簡單的解決方案了,因為攻擊者不能獲得第三方的Cookie(理論上),所以表單中的資料也就構造失敗了:>
<?php //構造加密的Cookie資訊 $value = “DefenseSCRF”; setcookie(”cookie”, $value, time()+3600); ?>
在表單裡增加Hash值,以認證這確實是使用者傳送的請求。
<?php $hash = md5($_COOKIE['cookie']); ?> <form method=”POST” action=”transfer.php”> <input type=”text” name=”toBankId”> <input type=”text” name=”money”> <input type=”hidden” name=”hash” value=”<?=$hash;?>”> <input type=”submit” name=”submit” value=”Submit”> </form>
然後在伺服器端進行Hash值驗證
<?php if(isset($_POST['check'])) { $hash = md5($_COOKIE['cookie']); if($_POST['check'] == $hash) { doJob(); } else { //... } } else { //... } ?>
這個方法網上說已經可以杜絕99%的CSRF攻擊了,那還有1%呢....由於使用者的Cookie很容易由於網站的XSS漏洞而被盜取,這就另外的1%。一般的攻擊者看到有需要算Hash值,基本都會放棄了,某些除外,所以如果需要100%的杜絕,這個不是最好的方法。
(2).驗證碼
這個方案的思路是:每次的使用者提交都需要使用者在表單中填寫一個圖片上的隨機字串,厄....這個方案可以完全解決CSRF,但個人覺得在易用性方面似乎不是太好,還有聽聞是驗證碼圖片的使用涉及了一個被稱為MHTML的Bug,可能在某些版本的微軟IE中受影響。
(3).One-Time Tokens(不同的表單包含一個不同的偽隨機值)
在實現One-Time Tokens時,需要注意一點:就是“並行會話的相容”。如果使用者在一個站點上同時開啟了兩個不同的表單,CSRF保護措施不應該影響到他對任何表單的提交。考慮一下如果每次表單被裝入時站點生成一個偽隨機值來覆蓋以前的偽隨機值將會發生什麼情況:使用者只能成功地提交他最後開啟的表單,因為所有其他的表單都含有非法的偽隨機值。必須小心操作以確保CSRF保護措施不會影響選項卡式的瀏覽或者利用多個瀏覽器視窗瀏覽一個站點。
以下是舉例實現:
1).先是令牌生成函式(gen_token()):
<?php function gen_token() { //這裡我是貪方便,實際上單使用Rand()得出的隨機數作為令牌,也是不安全的。 //這個可以參考我寫的Findbugs筆記中的《Random object created and used only once》 $token = md5(uniqid(rand(), true)); return $token; } ?>
2).然後是Session令牌生成函式(gen_stoken()):
<?php function gen_stoken() { $pToken = ""; if($_SESSION[STOKEN_NAME] == $pToken){ //沒有值,賦新值 $_SESSION[STOKEN_NAME] = gen_token(); }else{ //繼續使用舊的值 } } ?>
3).WEB 表單生成隱藏輸入域的函式:
<?php function gen_input() { gen_stoken(); echo “<input type=\”hidden\” name=\”" . FTOKEN_NAME . “\” value=\”" . $_SESSION[STOKEN_NAME] . “\”> “; } ?>
4).WEB表單結構:
<?php session_start(); include(”functions.php”); ?> <form method=”POST” action=”transfer.php”> <input type=”text” name=”toBankId”> <input type=”text” name=”money”> <? gen_input(); ?> <input type=”submit” name=”submit” value=”Submit”> </FORM>
5).服務端核對令牌:
這個很簡單,這裡就不再囉嗦了。
上面這個其實不完全符合“並行會話的相容”的規則,大家可以在此基礎上修改。