跨站點請求偽造解決方案
近期通過APPScan掃描程序,發現了不少安全問題,通過大量查閱和嘗試最終還是解決掉了,於是整理了一下方便查閱。
前一篇博客介紹了啟用了不安全的HTTP方法
的解決方案,有興趣請移步http://www.cnblogs.com/xlyslr/p/5707995.html。
1.跨站點請求偽造
首先,什么是跨站點請求偽造?
跨站點請求偽造-CSRF(Cross Site Request Forgery):是一種網絡攻擊方式。
說的白話一點就是,別的站點偽造你的請求,最可怕的是你還沒有察覺並且接收了。聽起來確實比較危險,下面有個經典的實例,了解一下跨站點請求偽造到底是怎么是實現的,知己知彼。
受害者:Bob
黑客:Mal
銀行:bank
bob在銀行有一筆存款,可以通過請求http://bank.example/withdraw?account=bob&amount=1000000&for=bob2
把錢轉到bob2下。通常情況下,該請求到達網站后,服務器會驗證請求是否來自一個合法的session,並且該session的用戶Bob已登錄。Mal在該銀行也有賬戶,於是他偽造了一個地址http://bank.example/withdraw?account=bob&amount=1000000&for=mal
,但是如果直接訪問,服務器肯定會識別出當前登錄用戶是mal而不是Bob,不能接受請求。於是通過CSRF攻擊方式,將此鏈接偽造在廣告下,誘使Bob自己點這個鏈接,那么請求就會攜帶Bob瀏覽起的cookie一起發送到銀行,而Bob同時又登錄了銀行或者剛剛登錄不久session還沒有過期,那服務器發現cookie中有Bob的登錄信息,就接收了響應,攻擊就成功了
2.現在主要的幾種防御CSRF的策略:
1. 驗證Referer:
referer攜帶請求來源,從示例可以看出,受害者發送非法請求肯定不是在銀行的界面,所以在服務器通過驗證Referer是不是bank.example
開始就可以了,這個方法簡單粗暴。
最簡單的實現就是加個Filter:
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException
- {
- String referer=request.getHeader("Referer");
- if((referer!=null) &&(referer.trim().startsWith("bank.example"))){
- chain.doFilter(request, response);
- }else{
- request.getRequestDispatcher("error.jsp").forward(request,response);
- }
- }
2. 在請求參數中添加token驗證:
要抵御跨站點請求偽造就要設置一個黑客偽造不了的東西。我們可以在請求參數中加一個隨機token,在服務器驗證這個token,通過即銷毀重設。下面說一下我的實現:
首先定義token為key-value
結構,因為很多情況會從不同的地方訪問同一個請求,如果是單一的數據結構,第一個請求生成token后還沒來得及發送請求,第二個又請求生成token就會把第一個沖掉,從而導致連續的驗證失敗。所以,我們要通過請求源將token隔離起來。這里我將請求地址摘要后作為token的key,用GUID作為token的value,代碼如下:
- /**
- * 根據請求地址獲取token-key
- */
- public static String getTokenKey(HttpServletRequest request){
- String key = null;
- try {
- MessageDigest mDigest = MessageDigest.getInstance("MD5");//摘要算法可以自己選擇
- byte[] result = mDigest.digest(request.getRequestURL().toString().getBytes());
- key = StringUtil.bytes2hex(result);
- } catch (NoSuchAlgorithmException e) {
- LOGGER.error("get token key failed",e);
- }
- return key
- }
-
- /**
- * 獲取token-value並存儲在session中
- */
- public static String getTokenValue(HttpServletRequest request){
- String key = getTokenKey(request);
- Map<String,String> tokenMap = null;
- Object obj = request.getSession().getAttribute("tokenMap");
- if(obj == null){
- tokenMap = new HashMap<String,String>();
- request.getSession().setAttribute("tokenMap", tokenMap);
- } else {
- tokenMap = (Map<String,String>)obj;
- }
- if(tokenMap.containsKey(key)){
- return tokenMap.get(key);
- }
- String value = GUID.generate();//GUID實現可自行百度,其實弄個偽隨機數也是可以的...
- tokenMap.put(key,value);
- return value;
- }
-
- /**
- * 驗證token
- */
- public static boolean verify(String key ,String value ,HttpServletRequest request){
- boolean result = false;
- if (StringUtil.isEmpty(key) || StringUtil.isEmpty(value)) {//key或value只要有一個不存在就驗證不通過
- return result;
- }
-
- if (request.getSession() != null) {
- Map<String,String> tokenMap = getTokenMap(request);
- if(value.equals(tokenMap.get(key))){
- result = true;
- tokenMap.remove(key);//成功一次就失效
- }
- }
- return result;
- }
完成上邊的工具方法后,需要在form中添加token,如下:
- <form name="frm" action="/test/tokentest.htm" method="POST">
- <input type="hidden" name="token_key" value="<%=Token.getTokenKey(request) %>"/>
- <input type="hidden" name="token_value" value="<%=Token.getTokenValue(request) %>"/>
- ...
- </form>
驗證可以放在Filter里也可以放在Service里,只要保證請求/test/tokentest.htm
會先驗證就行了。直接調用工具方法Token.verify()
以下就不贅述了。
3. 在HTTP頭中自定義屬性並驗證:
這個方法和上面那個類似,也是設置token,只是把token設置為HTTP頭中的自定義屬性。
通過XMLHttpRequest可以一次性給所有該類請求的HTTP頭加上token 屬性,但是XMLHttpRequest請求通常用於Ajax方法對局部頁面的異步刷新,比較有局限性;而且通過XMLHttpRequest請求的地址不會被記錄到瀏覽器的地址欄,一方面不會通過Referer泄露token,另一方面會導致前進,后退,刷新,收藏等操作失效,所以還是慎用。
雖然上面介紹了幾種方法,但現在還沒有一種完美的解決方案,但是通過Referer和Token方案結合起來使用,也能很得有效CSRF攻擊。