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

说说CSRF

对CSRF来说,其实Spring3.1、ASP.NET MVC3、Rails、Django等都已经支持自动在涉及POST的地方添加Token(包括FORM表单和AJAX POST等),似乎是一个tag的事情,但如果了解一些实现原理,手工来处理,也是有好处的。因为其实很多人做web开发,但涉及到web安全方面的都是比较资深的开发人员,很多人安全意识非常薄弱,CSRF是什么根本没有听说过。所以对他们来说,CSRF已经是比较高深的东西了。先说说什么是CSRF?你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。CSRF一般都是利用你的已登录已验证的身份来发送恶意请求。比较著名的一个例子就是2009年黑客利用Gmail的一个CSRF漏洞成功获取好莱坞明星Vanessa Hudgens的独家艳照。其攻击过程非常简单,给该明星的gmail账户发了一封email,标题是某大导演邀请你来看看这个电影,里面有个图片:,结果她登录Gmail,打开邮件就默默无闻的中招了,所有邮件被转发到黑客的账号。因为当时Gmail设置转发的设置页面有漏洞,其设置方法是打开一个窗口,点击确定后实际URL是https://mail.google.com/[email protected]:

其实即使不是在同一个页面打开,在不同的tab打开也是一样可以通过网站登录验证的,因为受害者首先已经登录了网站,在浏览网站的过程中,若网站设置了Session cookie,那么在浏览器进程的生命周期内,即使浏览器同一个窗口打开了新的tab页面,Session cookie也都是有效的,他们在浏览器同一个窗口的多个tab页面里面是共享的(注:现在Gmail支持多个tab同时持有多个SessionID)。所以攻击步骤是,第一,受害者必须在同一浏览器窗口(即使不是同一tab)内访问并登陆目标站点;第二,这使得Session cookie有效,从而利用受害者的身份进行恶意操作。 

再举个实际的例子,假设我们界面上有删除某一项的链接,例如:Delete

其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";

}

点击界面上那个Delete链接,就后台删除某项,看起来非常正常啊。 

好,现在你登录你的网站,然后在另外一个tab打开这个html文件:







    

    hack





  

 

发现同样被删除了某项。试想,如果是网银,你的钱已经被转账......(除了referer不一样,session cookie被利用)

Spring MVC防御CSRF、XSS和SQL注入攻击_第1张图片Spring MVC防御CSRF、XSS和SQL注入攻击_第2张图片

好了,现在 后台改成POST(写操作尽量用POST),前台界面那个删除的链接改成Form提交:

看起来安全多了。OK,现在你登录你的网站,然后在另外一个tab打开这个html文件:







    Hack

    

  

  

发现同样被删除了某项。试想,如果是网银,你的钱已经被转账......

当然,你如果前台还是用链接,但改成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: 

然后在后台提交的时候验证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加上:


   defaultHtmlEscape
   true

Forms加上:

更多信息查看OWASP的页面

第二种方法是手动escape,例如用户可以输入: 或者输入

abc

,如果有异常,显然有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会被替换,如果参数里面 含有敏感词会被替换掉:



     XssSqlFilter

     com.ibm.web.beans.XssFilter

  

  

     XssSqlFilter

     /*

     REQUEST

  

 (这个Filter也可以防止SQL注入攻击) 

假设登录页面有个输入用户名和密码的输入框,可以有很多Xss/csrf/注入钓鱼网站/SQL等的攻击手段,例如:

 输入用户名 :    >"'>
 输入用户名:     usera>"'>
 输入用户名:     "'>
 输入用户名:     usera'">