Spring MVC防御CSRF、XSS和SQL注入攻擊


本文說一下SpringMVC如何防御CSRF(Cross-site request forgery跨站請求偽造)和XSS(Cross site script跨站腳本攻擊)。

說說CSRF

CSRF來說,其實Spring3.1ASP.NET MVC3RailsDjango等都已經支持自動在涉及POST的地方添加Token(包括FORM表單和AJAX POST等),似乎是一個tag的事情,但如果了解一些實現原理,手工來處理,也是有好處的。因為其實很多人做web開發,但涉及到web安全方面的都是比較資深的開發人員,很多人安全意識非常薄弱,CSRF是什么根本沒有聽說過。所以對他們來說,CSRF已經是比較高深的東西了。先說說什么是CSRF?你這可以這么理解CSRF攻擊:攻擊者盜用了你的身份,以你的名義發送惡意請求。CSRF能夠做的事情包括:以你名義發送郵件,發消息,盜取你的賬號,甚至於購買商品,虛擬貨幣轉賬......造成的問題包括:個人隱私泄露以及財產安全。CSRF一般都是利用你的已登錄已驗證的身份來發送惡意請求。比較著名的一個例子就是2009年黑客利用Gmail的一個CSRF漏洞成功獲取好萊塢明星Vanessa Hudgens的獨家艷照。其攻擊過程非常簡單,給該明星的gmail賬戶發了一封email,標題是某大導演邀請你來看看這個電影,里面有個圖片:<img src="https://mail.google.com/mail?ui=2&fw=true&fwe=hacker@email.com">,結果她登錄Gmail,打開郵件就默默無聞的中招了,所有郵件被轉發到黑客的賬號。因為當時Gmail設置轉發的設置頁面有漏洞,其設置方法是打開一個窗口,點擊確定后實際URL是https://mail.google.com/mail?ui=2&fw=true&fwe=newMail@email.com:

 

其實即使不是在同一個頁面打開,在不同的tab打開也是一樣可以通過網站登錄驗證的,因為受害者首先已經登錄了網站,在瀏覽網站的過程中,若網站設置了Session cookie,那么在瀏覽器進程的生命周期內,即使瀏覽器同一個窗口打開了新的tab頁面,Session cookie也都是有效的,他們在瀏覽器同一個窗口的多個tab頁面里面是共享的(注:現在Gmail支持多個tab同時持有多個SessionID)。所以攻擊步驟是,第一,受害者必須在同一瀏覽器窗口(即使不是同一tab)內訪問並登陸目標站點;第二,這使得Session cookie有效,從而利用受害者的身份進行惡意操作。

再舉個實際的例子,假設我們界面上有刪除某一項的鏈接,例如:<a href="javascript:void(0)" onclick="region_del.do?name=0000001">Delete</a>;

其Java Spring MVC后台有個函數是刪除某個item,注意是GET不是POST:

@RequestMapping(value = "region_del.do", method = RequestMethod.GET)
public String regionDel(@RequestParam String name, Locale locale)
{
     // Delete region name=@name....
        
     return "redirect:/region.html";
}

點擊界面上那個<a href="javascript:void(0)" onclick="region_del.do?name=0000001">Delete</a>鏈接,就后台刪除某項,看起來非常正常啊。

好,現在你登錄你的網站,然后在另外一個tab打開這個html文件:

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html  xmlns ="http://www.w3.org/1999/xhtml"  xml:lang ="en"  lang ="en" >
< head >
     < meta  http-equiv ="Content-Type"  content ="text/html; charset=utf-8"   />
     < title >hack </ title >
</ head >
< body >
   < img  src ="http://localhost/testsite/region_del.do?name=0000001" />
  </ body >
</ html >

發現同樣被刪除了某項。試想,如果是網銀,你的錢已經被轉賬......(除了referer不一樣,session cookie被利用)

 

好了,現在 后台改成POST(寫操作盡量用POST),前台界面那個刪除的鏈接改成Form提交:

< form  action ="region_del.do"  method ="POST" >
  < input  type ="hidden"  name ="name"  value ="0000001" >
         < input  type ="submit"  value ="Delete"   />
</ form >

看起來安全多了。OK,現在你登錄你的網站,然后在另外一個tab打開這個html文件:

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html  xmlns ="http://www.w3.org/1999/xhtml"  xml:lang ="en"  lang ="en" >
< head >
     < title >Hack </ title >
     < script >
      
function  steal(){
        
var  mySubmit  =  document.getElementById( ' steal_form ' );
        mySubmit.submit();
      }
    
</ script >
   </ head >
   < body  onload ='steal()' >
< form  id  = "steal_form"  method ="POST"  action ="http://localhost/testsite/region_del.do" >
    < input  type ="hidden"  name ="func"  value ="post" >
< input  type ="hidden"  name ="name"  value ="0000001" >
</ form >
   </ body >
</ html >

 

發現同樣被刪除了某項。試想,如果是網銀,你的錢已經被轉賬......

當然,你如果前台還是用鏈接,但改成js,用AJAX POST提交,也是一樣的效果:

 

$.ajax({
 type: "POST",
 url:....
});

解決辦法就是在Form表單加一個hidden field,里面是服務端生成的足夠隨機數的一個Token,使得黑客猜不到也無法仿照Token

先寫一個類,生成足夠隨機數的Token(注:Java的Random UUID已經足夠隨機了,參考這個這個

package com.ibm.cn.web.beans;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
* A manager for the CSRF token for a given session. The {
@link  #getTokenForSession(HttpSession)} should used to
* obtain the token value for the current session (and this should be the only way to obtain the token value).
* **
*/

public  final  class CSRFTokenManager {

     /**
     * The token parameter name
     
*/
     static  final String CSRF_PARAM_NAME = "CSRFToken";

     /**
     * The location on the session which stores the token
     
*/
     public  static  final  String CSRF_TOKEN_FOR_SESSION_ATTR_NAME = CSRFTokenManager. class
            .getName() + ".tokenval";

     public  static String getTokenForSession(HttpSession session) {
        String token =  null;
        
         //  I cannot allow more than one token on a session - in the case of two
        
//  requests trying to
        
//  init the token concurrently
         synchronized (session) {
            token = (String) session
                    .getAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME);
             if ( null == token) {
                token = UUID.randomUUID().toString();
                session.setAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME, token);
            }
        }
         return token;
    }

     /**
     * Extracts the token value from the session
     * 
     * 
@param  request
     * 
@return
     
*/
     public  static String getTokenFromRequest(HttpServletRequest request) {
         return request.getParameter(CSRF_PARAM_NAME);
    }

     private CSRFTokenManager() {
    };

}

打開Form頁面的時候在服務端生成Token並保存到Session中,例如:model.addAttribute("csrf", CSRFTokenManager.getTokenForSession(this.session));

然后在Form中添加Hidden field: 

<input type="hidden" name="CSRFToken" value="${csrf}" />

然后在后台提交的時候驗證token :

 

@RequestMapping(value = "region_del.do", method = RequestMethod.GET)
public String regionDel(@RequestParam String name, @RequestParam String CSRFToken, Locale locale)
    {
         if(CSRFToken ==  null || !CSRFToken.equals(session.getAttribute(CSRFTokenManager.CSRF_TOKEN_FOR_SESSION_ATTR_NAME).toString())){
                logger.debug("CSRF attack detected. URL: region_edit.do");
                 return "redirect:/login.form";
        } 
                
     // Delete region name=@name....
        
     return "redirect:/region.html";
}

 

你還可以把上面的步驟寫到BaseController里面,或者寫到攔截器里面,攔截所有POST請求,驗證CSRF Token。這里掠過....

如果你用AJAX POST的方法,那么后台一樣,前台也要有Hidden field保存Token,然后在提交AJAX POST的時候加上該csrf參數即可。(更多csrf參考這個這個。)

 

AJAX POST的CSRF防御

 

首先在頁面進入的時候從后台生成一個Token(每個session),放到一個Hidden input(用Spring tag或freemarker可以寫) 。然后在ajax post提交的時候放到http請求的header里面:

     var headers = {};
    headers['__RequestVerificationToken'] = $("#CSRFToken").val();
    
    $.ajax({
        type: "POST",
        headers: headers,
        cache:  false,
        url: base + "ajax/domain/delete.do",
        data: "id=123",
        dataType:"json",
        async:  true,
        error:  function(data, error) {},
        success:  function(data)
        {
            
        }
    });

然后在后台controller里面校驗header里面這個token,也可以把這個函數放到baseController里面:

protected  boolean isValidCsrfHeaderToken() {
         if (getRequest().getHeader("__RequestVerificationToken") ==  null
                || session
                        .getAttribute(CSRFTokenManager.CSRF_TOKEN_FOR_SESSION_ATTR_NAME) ==  null
                || ! this.getRequest()
                        .getHeader("__RequestVerificationToken")
                        .equals(session
                                .getAttribute(
                                        CSRFTokenManager.CSRF_TOKEN_FOR_SESSION_ATTR_NAME)
                                .toString())) {
             return  false;
        }
         return  true;
    }

 

xss

關於xss的介紹可以看這個這個網頁,具體我就講講Spring MVC里面的預防:

web.xml加上:

< context-param >
    < param-name >defaultHtmlEscape </ param-name >
    < param-value >true </ param-value >
</ context-param >

Forms加上:

 

<spring:htmlEscape defaultHtmlEscape="true" />

 

更多信息查看OWASP頁面

 

第二種方法是手動escape,例如用戶可以輸入:<script>alert()</script> 或者輸入<h2>abc<h2>,如果有異常,顯然有xss漏洞。

首先添加一個jar包:commons-lang-2.5.jar ,然后在后台調用這些函數:StringEscapeUtils.escapeHtml(string); StringEscapeUtils.escapeJavaScript(string); StringEscapeUtils.escapeSql(string);

前台js調用escape函數即可。

 

第三種方法是后台加Filter,對每個post請求的參數過濾一些關鍵字,替換成安全的,例如:< > ' " \ /  # &

方法是實現一個自定義的HttpServletRequestWrapper,然后在Filter里面調用它,替換掉getParameter函數即可。

首先添加一個XssHttpServletRequestWrapper:

package com.ibm.web.beans;

import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public  class XssHttpServletRequestWrapper  extends HttpServletRequestWrapper {  
     public XssHttpServletRequestWrapper(HttpServletRequest servletRequest) {
         super(servletRequest);
    }
     public String[] getParameterValues(String parameter) {
      String[] values =  super.getParameterValues(parameter);
       if (values== null)  {
                   return  null;
          }
       int count = values.length;
      String[] encodedValues =  new String[count];
       for ( int i = 0; i < count; i++) {
                 encodedValues[i] = cleanXSS(values[i]);
       }
       return encodedValues;
    }
     public String getParameter(String parameter) {
          String value =  super.getParameter(parameter);
           if (value ==  null) {
                  return  null;
                  }
           return cleanXSS(value);
    }
     public String getHeader(String name) {
        String value =  super.getHeader(name);
         if (value ==  null)
             return  null;
         return cleanXSS(value);
    }
     private String cleanXSS(String value) {
                 // You'll need to remove the spaces from the html entities below
        value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;");
        value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;");
        value = value.replaceAll("'", "& #39;");
        value = value.replaceAll("eval\\((.*)\\)", "");
        value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
        value = value.replaceAll("script", "");
         return value;
    }

然后添加一個過濾器XssFilter :

package com.ibm.web.beans;

import java.io.IOException;  

import javax.servlet.Filter;  
import javax.servlet.FilterChain;  
import javax.servlet.FilterConfig;  
import javax.servlet.ServletException;  
import javax.servlet.ServletRequest;  
import javax.servlet.ServletResponse;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;

public  class XssFilter  implements Filter {
    FilterConfig filterConfig =  null;

     public  void init(FilterConfig filterConfig)  throws ServletException {
         this.filterConfig = filterConfig;
    }

     public  void destroy() {
         this.filterConfig =  null;
    }

     public  void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain)  throws IOException, ServletException {
        chain.doFilter( new XssHttpServletRequestWrapper(
                (HttpServletRequest) request), response);
    }
}

最后在web.xml里面配置一下,所有的請求的getParameter會被替換,如果參數里面 含有敏感詞會被替換掉:

   < filter >
      < filter-name >XssSqlFilter </ filter-name >
      < filter-class >com.ibm.web.beans.XssFilter </ filter-class >
   </ filter >
   < filter-mapping >
      < filter-name >XssSqlFilter </ filter-name >
      < url-pattern >/* </ url-pattern >
      < dispatcher >REQUEST </ dispatcher >
   </ filter-mapping >

 (這個Filter也可以防止SQL注入攻擊) 

 

登錄頁面的攻擊例子

假設登錄頁面有個輸入用戶名和密碼的輸入框,可以有很多Xss/csrf/注入釣魚網站/SQL等的攻擊手段,例如:

 

 輸入用戶名 :    >"'><script>alert(1779)</script>
 輸入用戶名:     usera>"'><img src="javascript:alert(23664)">
 輸入用戶名:     "'><IMG SRC="/WF_XSRF.html--end_hig--begin_highlight_tag--hlight_tag--">
 輸入用戶名:     usera'"><iframe src=http://demo.testfire.net--en--begin_highlight_tag--d_highlight_tag-->

 

 

Web安全漏洞檢測工具

推薦使用IBM Rational AppScan(IBM Rational AppScan下載、版權購買和破解、注冊碼自己解決)

 

可以錄制腳本,設置URL,如果網站需要登錄,可以設置自動登錄:

 

檢測結果還可以保持為專業的pdf檢測報告

 

 

業界安全編程標准最佳實踐


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM