什么是CSRF攻击

这个网上一大把解释,直接google就可以了。这里就不赘述了。

如何防御

目前对CSRF的基本都是一种处理方式——使用token校验。简单来说,就是对于每一个需要做CSRF检查的请求(一般是POST请求),服务端会根据一定的策略分配一个token(或者前端js生成。比较少见,因为这样token的算法暴露了。当然如果token需要的输入攻击者拿不到的话,问题不大。),这样当页面提交的时候,服务端判断该请求是不是需要做CSRF检查,如果是,则会拿用户提交的token跟后端保存的token(一般是在session中)或者用同样算法计算出来的token做比较,如果不同,则认为是CSRF攻击。

TIPS

下面的一些做法,可以在一定程度上提高CSRF攻击的难度:

  1. 任何修改操作,不要用GET请求。但是POST请求也是很容易构造的。
  2. 服务端对提交页面进行Referer的合法性验证,可以在一定程度上避免这种CSRF攻击。但是Referer是可以伪造。目前已知历史上flash多次出现可伪造referer的漏洞。同时,有些浏览器、防火墙、代理或许会过滤原始请求的referer导致应用异常。不过如果没有referer伪造漏洞,检查referer不失为一种轻量级的防御CSRF攻击的办法。需要特别注意的是应该匹配顶级域名。比如不能简单的正则匹配arganzheng.me,黑客只要将攻击域名设置为api.arganzheng.me.herker.com,就很容易就绕过了。

实现

思路一: 服务端分配csrfToken

token一般存放在session中,这也是很多框架的默认实现。不过分布式环境下应该使用Redis这样的集中式缓存进行token的存放,token的过期时间可以根据业务需要设置。优点是客户端无感知,缺点是服务端有状态。

这个可以

思路二: 服务端和客户端通过一样的算法和输入计算token值,进行比较

在所有的请求中添加一个g_tk参数,用于判断用户的真伪。g_tk由客户端skey(sessionKey, 存放在cookies中)进行time33加密生成,在服务端对skey进行同样的操作后,判断g_tk的一致性。

1. 前端JS库提供四个基础函数:

  • addToken: 添加token的函数
  • getCookies:获取cookie的函数
  • time33: time33算法函数
  • getToken: 获取token函数

具体实现如下:

/**
 * type标识请求的方式,j132标识jquery,j126标识base,lk标识普通链接,fr标识form表单
 *
function $addToken(url,type){
	var token=$getToken();
	return token==""?url:url+(url.indexOf("?")!=-1?"&":"?")+"g_tk="+token+"&g_ty="+type;
}

function $getToken(){
	var skey=$getCookie("skey"),
		token=skey==null?"":$time33(skey);
	return token;
}

function $getCookie(name){
    //读取COOKIE
	var reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)"),
		val=document.cookie.match(reg);//如果获取不到会提示null
	return val?unescape(val[2]):null;
};

function $time33(str){
    //哈希time33算法
    for(var i = 0, len = str.length,hash = 5381; i < len; ++i){
       hash += (hash << 5) + str.charAt(i).charCodeAt();
    };
    return hash & 0x7fffffff;
};

针对不同的请求,token的提交方式如下:

  1. form表单提交:遍历页面中所有的form表单,并修改form的原始action,在action后自动添加token数据

     <script type="text/javascript">
     $(document).ready(function(){
         var forms=document.getElementsByTagName("form");
     	    for(var i=0,len=forms.length;i<len;i++){
             forms[i].action=$addToken(forms[i].action,"fr");
         };
     });
     </script>
    
  2. ajax:使用getToken手动追加
  3. 超链接提交:使用$addToken对a链接的href进行手动处理。

2. 后台拦截器做校验

package me.arganzheng.study.csrf.util;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import me.arganzheng.study.csrf.exception.CsrfTokenInvalidException;

/**
 * @author arganzheng
 * @date 2013-1-14
 */
public class CsrfTokenCheckerInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private User user;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!request.getMethod().equalsIgnoreCase("POST")) {
            // Not a POST - allow the request
            return true;
        }
        // 默认所有POST都需要检查(后面可以考虑试用anotation来区别。但是Spring MVC 3.1之前的版本Interceptor中拿不到controller的信息,没法做到method级别的控制。。)
        String token = getCsrfToken(user.getSkey());
        if (StringUtils.equals(token, request.getParameter("g_tk"))) {
            return true;
        } else {
            throw new CsrfTokenInvalidException();
        }
    }

    public String getCsrfToken(String skey) {
        return time33(skey);
    }

    public String time33(String skey) {
        if (skey == null) return null;

        int hash = 5381;
        for (int i = 0, len = skey.length(); i < len; ++i) {
            int cc = skey.charAt(i);
            hash += (hash << 5) + cc;
        }
        hash &= 0x7fffffff;
        return String.valueOf(hash);
    }
    	    	    
}

说明

  1. 由于token是根据skey动态算出来的,成本不大,所以这里不需要保存token,每次都是重新计算好了。
  2. 当然,token也可以是服务器在页面渲染之间计算并放入的。这样客户端就没有逻辑了。
  3. 由于COOKIE不可跨域,所以第三方网站不可能知道计算出正确的TOKEN值,服务器后台校验TOKEN,即可在SESSION级别在客户端一层防止CSRF。但是,假如黑客拦截了请求,得到sessionKey,那么在用户这次SESSION中,请求是可以被伪造的,上面方式就被破解了。根据业务需要,可以将SESSION级别的CSRF升级为请求级别的CSRF。
  4. skey本身类似于一个sessionId,具有时间性,根据传递性,csrf token也具有时间性。
  5. 拦截器这里简单对所有POST进行CSRF校验,其实可以使用 anotation 进行更细粒度的控制,但是Spring MVC 3.1之前的版本Interceptor中拿不到controller的信息,没法做到method级别的控制。

使用anotation可以这么写:

package me.arganzheng.study.csrf.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckCsrfToken {

    boolean required() default true;

}

然后在interceptor中这样判断:

HandlerMethod method = (HandlerMethod)handler;
if(method.getMethodAnnoation(CheckCsrfToken.class)!=null){
    // check csrf ...
}

—EOF—