什么是CSRF攻击
这个网上一大把解释,直接google就可以了。这里就不赘述了。
如何防御
目前对CSRF的基本都是一种处理方式——使用token校验。简单来说,就是对于每一个需要做CSRF检查的请求(一般是POST请求),服务端会根据一定的策略分配一个token(或者前端js生成。比较少见,因为这样token的算法暴露了。当然如果token需要的输入攻击者拿不到的话,问题不大。),这样当页面提交的时候,服务端判断该请求是不是需要做CSRF检查,如果是,则会拿用户提交的token跟后端保存的token(一般是在session中)或者用同样算法计算出来的token做比较,如果不同,则认为是CSRF攻击。
TIPS
下面的一些做法,可以在一定程度上提高CSRF攻击的难度:
- 任何修改操作,不要用GET请求。但是POST请求也是很容易构造的。
- 服务端对提交页面进行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的提交方式如下:
-
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>
- ajax:使用getToken手动追加
- 超链接提交:使用$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);
}
}
说明
- 由于token是根据skey动态算出来的,成本不大,所以这里不需要保存token,每次都是重新计算好了。
- 当然,token也可以是服务器在页面渲染之间计算并放入的。这样客户端就没有逻辑了。
- 由于COOKIE不可跨域,所以第三方网站不可能知道计算出正确的TOKEN值,服务器后台校验TOKEN,即可在SESSION级别在客户端一层防止CSRF。但是,假如黑客拦截了请求,得到sessionKey,那么在用户这次SESSION中,请求是可以被伪造的,上面方式就被破解了。根据业务需要,可以将SESSION级别的CSRF升级为请求级别的CSRF。
- skey本身类似于一个sessionId,具有时间性,根据传递性,csrf token也具有时间性。
- 拦截器这里简单对所有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—