跨站請求偽造CSRF防護方法
CSRF(Cross-site request forgery跨站請求偽造,也被稱成為“one click attack”或者session riding,通常縮寫為CSRF或者XSRF,是一種對網(wǎng)站的惡意利用。
一、CSRF攻擊原理
CSRF攻擊原理比較簡單,如圖1所示。其中Web A為存在CSRF漏洞的網(wǎng)站,Web B為攻擊者構(gòu)建的惡意網(wǎng)站,User C為Web A網(wǎng)站的合法用戶。
圖1 CSRF攻擊原理
1. 用戶C打開瀏覽器,訪問受信任網(wǎng)站A,輸入用戶名和密碼請求登錄網(wǎng)站A;
2.在用戶信息通過驗證后,網(wǎng)站A產(chǎn)生Cookie信息并返回給瀏覽器,此時用戶登錄網(wǎng)站A成功,可以正常發(fā)送請求到網(wǎng)站A;
3. 用戶未退出網(wǎng)站A之前,在同一瀏覽器中,打開一個TAB頁訪問網(wǎng)站B;
4. 網(wǎng)站B接收到用戶請求后,返回一些攻擊性代碼,并發(fā)出一個請求要求訪問第三方站點A;
5. 瀏覽器在接收到這些攻擊性代碼后,根據(jù)網(wǎng)站B的請求,在用戶不知情的情況下攜帶Cookie信息,向網(wǎng)站A發(fā)出請求。網(wǎng)站A并不知道該請求其實是由B發(fā)起的,所以會根據(jù)用戶C的Cookie信息以C的權(quán)限處理該請求,導(dǎo)致來自網(wǎng)站B的惡意代碼被執(zhí)行。
二、CSRF漏洞防御
CSRF漏洞防御主要可以從三個層面進行,即服務(wù)端的防御、用戶端的防御和安全設(shè)備的防御。
1、 服務(wù)端的防御
.1.1 驗證HTTP Referer字段
根據(jù)HTTP協(xié)議,在HTTP頭中有一個字段叫Referer,它記錄了該HTTP請求的來源地址。在通常情況下,訪問一個安全受限頁面的請求必須來自于同一個網(wǎng)站。比如某銀行的轉(zhuǎn)賬是通過用戶訪問http://bank.test/test?page=10&userID=101&money=10000頁面完成,用戶必須先登錄bank.test,然后通過點擊頁面上的按鈕來觸發(fā)轉(zhuǎn)賬事件。當用戶提交請求時,該轉(zhuǎn)賬請求的Referer值就會是轉(zhuǎn)賬按鈕所在頁面的URL(本例中,通常是以bank. test域名開頭的地址)。而如果攻擊者要對銀行網(wǎng)站實施CSRF攻擊,他只能在自己的網(wǎng)站構(gòu)造請求,當用戶通過攻擊者的網(wǎng)站發(fā)送請求到銀行時,該請求的Referer是指向攻擊者的網(wǎng)站。因此,要防御CSRF攻擊,銀行網(wǎng)站只需要對于每一個轉(zhuǎn)賬請求驗證其Referer值,如果是以bank. test開頭的域名,則說明該請求是來自銀行網(wǎng)站自己的請求,是合法的。如果Referer是其他網(wǎng)站的話,就有可能是CSRF攻擊,則拒絕該請求。
1.2 在請求地址中添加token并驗證
CSRF攻擊之所以能夠成功,是因為攻擊者可以偽造用戶的請求,該請求中所有的用戶驗證信息都存在于Cookie中,因此攻擊者可以在不知道這些驗證信息的情況下直接利用用戶自己的Cookie來通過安全驗證。由此可知,抵御CSRF攻擊的關(guān)鍵在于:在請求中放入攻擊者所不能偽造的信息,并且該信息不存在于Cookie之中。鑒于此,系統(tǒng)開發(fā)者可以在HTTP請求中以參數(shù)的形式加入一個隨機產(chǎn)生的token,并在服務(wù)器端建立一個攔截器來驗證這個token,如果請求中沒有token或者token內(nèi)容不正確,則認為可能是CSRF攻擊而拒絕該請求。
1.3 在HTTP頭中自定義屬性并驗證
自定義屬性的方法也是使用token并進行驗證,和前一種方法不同的是,這里并不是把token以參數(shù)的形式置于HTTP請求之中,而是把它放到HTTP頭中自定義的屬性里。通過XMLHttpRequest這個類,可以一次性給所有該類請求加上csrftoken這個HTTP頭屬性,并把token值放入其中。這樣解決了前一種方法在請求中加入token的不便,同時,通過這個類請求的地址不會被記錄到瀏覽器的地址欄,也不用擔心token會通過Referer泄露到其他網(wǎng)站。
2、 其他防御方法
1. CSRF攻擊是有條件的,當用戶訪問惡意鏈接時,認證的cookie仍然有效,所以當用戶關(guān)閉頁面時要及時清除認證cookie,對支持TAB模式(新標簽打開網(wǎng)頁)的瀏覽器尤為重要。
2. 盡量少用或不要用request()類變量,獲取參數(shù)指定request.form()還是request. querystring (),這樣有利于阻止CSRF漏洞攻擊,此方法只不能完全防御CSRF攻擊,只是一定程度上增加了攻擊的難度。
代碼示例:
Java 代碼示例
下文將以 Java 為例,對上述三種方法分別用代碼進行示例。無論使用何種方法,在服務(wù)器端的攔截器必不可少,它將負責檢查到來的請求是否符合要求,然后視結(jié)果而決定是否繼續(xù)請求或者丟棄。在 Java 中,攔截器是由 Filter 來實現(xiàn)的。我們可以編寫一個 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 開頭時,則繼續(xù)請求,否則的話可能是 CSRF 攻擊,轉(zhuǎn)到 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){ // 產(chǎn)生新的 token 放入 session 中 sToken = generateToken(); s.setAttribute(“csrftoken”,sToken); chain.doFilter(request, response); } else{ // 從 HTTP 頭中取得 csrftoken String xhrToken = req.getHeader(“csrftoken”); // 從請求參數(shù)中取得 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 之中,并繼續(xù)執(zhí)行請求。如果 session 中已經(jīng)有 csrftoken,則說明用戶已經(jīng)與服務(wù)器之間建立了一個活躍的 session,這時要看這個請求中有沒有同時附帶這個 token,由于請求可能來自于常規(guī)的訪問或是 XMLHttpRequest 異步訪問,我們分別嘗試從請求中獲取 csrftoken 參數(shù)以及從 HTTP 頭中獲取 csrftoken 自定義屬性并與 session 中的值進行比較,只要有一個地方帶有有效 token,就判定請求合法,可以繼續(xù)執(zhí)行,否則就轉(zhuǎn)到錯誤頁面。生成 token 有很多種方法,任何的隨機算法都可以使用,Java 的 UUID 類也是一個不錯的選擇。
除了在服務(wù)器端利用 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; // 動態(tài)生成 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 中已含有其他參數(shù) location = location + '&csrftoken=' + token; } else { //url 中沒有其他參數(shù) location = location + '?csrftoken=' + token; } if(fragment != null){ location += fragment; } element.setAttribute(attr, location); } }
在客戶端 html 中,主要是有兩個地方需要加上 token,一個是表單 form,另一個就是鏈接 a。這段代碼首先遍歷所有的 form,在 form 最后添加一隱藏字段,把 csrftoken 放入其中。然后,代碼遍歷所有的鏈接標記 a,在其 href 屬性中加入 csrftoken 參數(shù)。注意對于 a.href 來說,可能該屬性已經(jīng)有參數(shù),或者有錨標記。因此需要分情況討論,以不同的格式把 csrftoken 加入其中。
如果你的網(wǎng)站使用 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代碼示例:
請看下面一個簡單的應(yīng)用,它允許用戶購買鋼筆或鉛筆。界面上包含下面的表單:
<form action="buy.php" method="POST"> <p> Item: <select name="item"> <option name="pen">pen</option> <option name="pencil">pencil</option> </select><br /> Quantity: <input type="text" name="quantity" /><br /> <input type="submit" value="Buy" /> </p></form>
下面的buy.php程序處理表單的提交信息:
<?php session_start(); $clean = array(); if (isset($_REQUEST['item'] && isset($_REQUEST['quantity'])) { /* Filter Input ($_REQUEST['item'], $_REQUEST['quantity']) */ if (buy_item($clean['item'], $clean['quantity'])) { echo '<p>Thanks for your purchase.</p>'; } else { echo '<p>There was a problem with your order.</p>'; } }?>
攻擊者會首先使用這個表單來觀察它的動作。例如,在購買了一支鉛筆后,攻擊者知道了在購買成功后會出現(xiàn)感謝信息。注意到這一點后,攻擊者會嘗試通過訪問下面的URL以用GET方式提交數(shù)據(jù)是否能達到同樣的目的:
http://store.example.org/buy.php?item=pen&quantity=1
如果能成功的話,攻擊者現(xiàn)在就取得了當合法用戶訪問時,可以引發(fā)購買的URL格式。在這種情況下,進行跨站請求偽造攻擊非常容易,因為攻擊者只要引發(fā)受害者訪問該URL即可。
請看下面對前例應(yīng)用更改后的代碼:
php session_start(); $token = md5(uniqid(rand(), TRUE)); $_SESSION['token'] = $token; $_SESSION['token_time'] = time();?>
表單:
<form action="buy.php" method="POST"> <input type="hidden" name="token" value="<?php echo $token; ?>" /> <p> Item: <select name="item"> <option name="pen">pen</option> <option name="pencil">pencil</option> </select><br /> Quantity: <input type="text" name="quantity" /><br /> <input type="submit" value="Buy" /> </p></form>
通過這些簡單的修改,一個跨站請求偽造攻擊就必須包括一個合法的驗證碼以完全模仿表單提交。由于驗證碼的保存在用戶的session中的,攻擊者必須對每個受害者使用不同的驗證碼。這樣就有效的限制了對一個用戶的任何攻擊,它要求攻擊者獲取另外一個用戶的合法驗證碼。使用你自己的驗證碼來偽造另外一個用戶的請求是無效的。
該驗證碼可以簡單地通過一個條件表達式來進行檢查:
<?php if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token']) { /* Valid Token */ }?>
你還能對驗證碼加上一個有效時間限制,如5分鐘:
<?php $token_age = time() - $_SESSION['token_time']; if ($token_age <= 300) { /* Less than five minutes has passed. */ }?>
通過在你的表單中包括驗證碼,你事實上已經(jīng)消除了跨站請求偽造攻擊的風險。可以在任何需要執(zhí)行操作的任何表單中使用這個流程。