CSRF 攻擊的應對之道
web安全之token和CSRF攻擊
CSRF Token 的設(shè)計是否有其必要性滤否?
CSRF 背景與介紹
CSRF(Cross Site Request Forgery, 跨站域請求偽造)是一種網(wǎng)絡(luò)的攻擊方式,它在 2007 年曾被列為互聯(lián)網(wǎng) 20 大安全隱患之一钳枕。其他安全隱患缴渊,比如 SQL 腳本注入珍逸,跨站域腳本攻擊等在近年來已經(jīng)逐漸為眾人熟知模狭,很多網(wǎng)站也都針對他們進行了防御。然而卿啡,對于大多數(shù)人來說昔瞧,CSRF 卻依然是一個陌生的概念指蚁。即便是大名鼎鼎的 Gmail, 在 2007 年底也存在著 CSRF 漏洞,從而被黑客攻擊而使 Gmail 的用戶造成巨大的損失自晰。
CSRF 攻擊實例
CSRF 攻擊可以在受害者毫不知情的情況下以受害者名義偽造請求發(fā)送給受攻擊站點凝化,從而在并未授權(quán)的情況下執(zhí)行在權(quán)限保護之下的操作。比如說酬荞,受害者 Bob 在銀行有一筆存款搓劫,通過對銀行的網(wǎng)站發(fā)送請求 http://bank.example/withdraw?account=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款轉(zhuǎn)到 bob2 的賬號下。通常情況下混巧,該請求發(fā)送到網(wǎng)站后枪向,服務(wù)器會先驗證該請求是否來自一個合法的 session,并且該 session 的用戶 Bob 已經(jīng)成功登陸咧党。黑客 Mallory 自己在該銀行也有賬戶遣疯,他知道上文中的 URL 可以把錢進行轉(zhuǎn)帳操作。Mallory 可以自己發(fā)送一個請求給銀行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory凿傅。但是這個請求來自 Mallory 而非 Bob缠犀,他不能通過安全認證,因此該請求不會起作用聪舒。這時辨液,Mallory 想到使用 CSRF 的攻擊方式,他先自己做一個網(wǎng)站箱残,在網(wǎng)站中放入如下代碼: src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ”滔迈,并且通過廣告等誘使 Bob 來訪問他的網(wǎng)站。當 Bob 訪問該網(wǎng)站時被辑,上述 url 就會從 Bob 的瀏覽器發(fā)向銀行燎悍,而這個請求會附帶 Bob 瀏覽器中的 cookie 一起發(fā)向銀行服務(wù)器。大多數(shù)情況下盼理,該請求會失敗谈山,因為他要求 Bob 的認證信息。但是宏怔,如果 Bob 當時恰巧剛訪問他的銀行后不久奏路,他的瀏覽器與銀行網(wǎng)站之間的 session 尚未過期畴椰,瀏覽器的 cookie 之中含有 Bob 的認證信息。這時鸽粉,悲劇發(fā)生了斜脂,這個 url 請求就會得到響應,錢將從 Bob 的賬號轉(zhuǎn)移到 Mallory 的賬號触机,而 Bob 當時毫不知情帚戳。等以后 Bob 發(fā)現(xiàn)賬戶錢少了,即使他去銀行查詢?nèi)罩纠苁祝仓荒馨l(fā)現(xiàn)確實有一個來自于他本人的合法請求轉(zhuǎn)移了資金片任,沒有任何被攻擊的痕跡。而 Mallory 則可以拿到錢后逍遙法外椒舵。
CSRF 攻擊的對象
在討論如何抵御 CSRF 之前,先要明確 CSRF 攻擊的對象约谈,也就是要保護的對象笔宿。從以上的例子可知,CSRF 攻擊是黑客借助受害者的 cookie 騙取服務(wù)器的信任棱诱,但是黑客并不能拿到 cookie泼橘,也看不到 cookie 的內(nèi)容。另外迈勋,對于服務(wù)器返回的結(jié)果炬灭,由于瀏覽器同源策略的限制,黑客也無法進行解析靡菇。因此重归,黑客無法從返回的結(jié)果中得到任何東西,他所能做的就是給服務(wù)器發(fā)送請求厦凤,以執(zhí)行請求中所描述的命令鼻吮,在服務(wù)器端直接改變數(shù)據(jù)的值,而非竊取服務(wù)器中的數(shù)據(jù)较鼓。所以椎木,我們要保護的對象是那些可以直接產(chǎn)生數(shù)據(jù)改變的服務(wù),而對于讀取數(shù)據(jù)的服務(wù)博烂,則不需要進行 CSRF 的保護香椎。比如銀行系統(tǒng)中轉(zhuǎn)賬的請求會直接改變賬戶的金額,會遭到 CSRF 攻擊禽篱,需要保護畜伐。而查詢余額是對金額的讀取操作,不會改變數(shù)據(jù)躺率,CSRF 攻擊無法解析服務(wù)器返回的結(jié)果烤礁,無需保護讼积。
當前防御 CSRF 的幾種策略
目前業(yè)界應對 CSRF 攻擊有一些克制方法,但是每種方法都有利弊脚仔,沒有一種方法是完美的勤众。如何選擇合適的方法非常重要。如果網(wǎng)站是一個現(xiàn)有系統(tǒng)鲤脏,想要在最短時間內(nèi)獲得一定程度的 CSRF 的保護们颜,那么驗證 Referer 的方法是最方便的,要想增加安全性的話猎醇,可以選擇不支持低版本瀏覽器窥突,畢竟就目前來說,IE7+, FF3+ 這類高版本瀏覽器的 Referer 值還無法被篡改硫嘶。
-
驗證 HTTP Referer 字段
-
在 HTTP 頭中自定義屬性并驗證
-
在請求地址中添加 token 并驗證
CSRF 攻擊之所以能夠成功阻问,是因為黑客可以完全偽造用戶的請求,該請求中所有的用戶驗證信息都是存在于 cookie 中沦疾,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的 cookie 來通過安全驗證称近。要抵御 CSRF,關(guān)鍵在于在請求中放入黑客所不能偽造的信息哮塞,并且該信息不存在于 cookie 之中刨秆。
可以在 HTTP 請求中以參數(shù)的形式加入一個隨機產(chǎn)生的 token,并在服務(wù)器端建立一個攔截器來驗證這個 token忆畅,如果請求中沒有 token 或者 token 內(nèi)容不正確衡未,則認為可能是 CSRF 攻擊而拒絕該請求。
這種方法要比檢查 Referer 要安全一些家凯,token 可以在用戶登陸后產(chǎn)生并放于 session 之中缓醋,然后在每次請求時把 token 從 session 中拿出,與請求中的 token 進行比對绊诲,但這種方法的難點在于如何把 token 以參數(shù)的形式加入請求改衩。對于 GET 請求,token 將附在請求地址之后驯镊,這樣 URL 就變成 http://url?csrftoken=tokenvalue葫督。 而對于 POST 請求來說,要在 form 的最后加上 <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>板惑,這樣就把 token 以參數(shù)的形式加入請求了橄镜。但是,在一個網(wǎng)站中冯乘,可以接受請求的地方非常多洽胶,要對于每一個請求都加上 token 是很麻煩的,并且很容易漏掉,通常使用的方法就是在每次頁面加載時姊氓,使用 javascript 遍歷整個 dom 樹丐怯,對于 dom 中所有的 a 和 form 標簽后加入 token。這樣可以解決大部分的請求翔横,但是對于在頁面加載之后動態(tài)生成的 html 代碼读跷,這種方法就沒有作用,還需要程序員在編碼時手動添加 token禾唁。
該方法還有一個缺點是難以保證 token 本身的安全效览。特別是在一些論壇之類支持用戶自己發(fā)表內(nèi)容的網(wǎng)站,黑客可以在上面發(fā)布自己個人網(wǎng)站的地址荡短。由于系統(tǒng)也會在這個地址后面加上 token丐枉,黑客可以在自己的網(wǎng)站上得到這個 token,并馬上就可以發(fā)動 CSRF 攻擊掘托。為了避免這一點瘦锹,系統(tǒng)可以在添加 token 的時候增加一個判斷,如果這個鏈接是鏈到自己本站的闪盔,就在后面添加 token弯院,如果是通向外網(wǎng)則不加。不過锭沟,即使這個 csrftoken 不以參數(shù)的形式附加在請求之中抽兆,黑客的網(wǎng)站也同樣可以通過 Referer 來得到這個 token 值以發(fā)動 CSRF 攻擊识补。這也是一些用戶喜歡手動關(guān)閉瀏覽器 Referer 功能的原因族淮。
實例1:token通過請求參數(shù)或者http頭傳遞,服務(wù)端緩存在session中
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);
}
}
實例2:token通過請求參數(shù)傳遞凭涂,緩存在cookie中(適用于不允許使用session的系統(tǒng))
/**
* 驗證防止csrf攻擊cookie值csrfToken
*
* @param request 請求request
* @param paramName token參數(shù)名
* @param cookieName cookie名
* @return
*/
public static boolean verifyCsrfToken(HttpServletRequest request, String paramName, String cookieName) {
boolean result = false;
String csrfToken = request.getParameter(paramName);
Cookie cookies[] = request.getCookies();
if (csrfToken != null && cookies != null) {
for (Cookie cookie : cookies) {
if (StringUtils.equals(cookieName, cookie.getName())) {
result = StringUtils.equals(csrfToken, cookie.getValue());
break;
}
}
}
return result;
}