CSRF的防御解决过程

CSRF是什么,就不多说,网络上的帖子多的去了,关于其定义。

这里主要介绍我们项目中,是如何解决这个问题的。方案比较简单,重点是介绍和记录一下遇到的问题和一些小的心得。

1. 解决方案

A. 用户登录的时候,将创建一个token,此token存放于session当中。(是否在登录后创建token,依据各自系统需求变化)

B. 基于Filter,对所有的Http请求进行拦截,捕获请求路径,确认路径URL是否在配置的CSRF安全拦截路径列表CsrfList中。

C. 若在CsrfList中,则检查session中是否含有sToken字段以及Http请求头中是否含有rToken字段。

D. 若sToken和rToken相等,则认为安全合法的请求,否则将请求拦截,拒绝此次请求。

这里:

1》. 主要是基于过滤器Filter来实现,另外,一个比较核心的思想,是将安全路径(需要校验的,比如系统参数相关的增删改相关的数据提交请求)通过配置的方式,以配置文件或者数据库表的形式配置在系统中(本案例,采用的是静态配置文件)。说白了,和Shiro或者Spring security的权限管理很像。

2》. 另外一点,将后台生成的token数据传递前端,并在前端有数据提交的时候将这个token值带回到后台。笨一点的办法,就是在每次ajax数据提交的时候,都给调用beforeSend方法给XMLHttpRequest里面添加自定义的Header属性(当然,也可以通过其他方式实现token的回传到后台,我这里采用的是Http的自定义Header属性的模式)。最好是有一个全局的配置,至少是文件级别的配置,减少ajax提交数据的时候写入重复的beforeSend调用。

2. 核心代码

核心代码,分Filter后端的部分,以及前端的beforeSend调用部分。

1》. Filter对应的后端部分

package com.tk.logc.core.csrf;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set; 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 org.apache.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject; import com.google.gson.Gson;
import com.tk.logc.core.Constants;
import com.tk.logc.core.ResultData; /**
* @author shihuc
* @date 2018年8月21日 上午9:23:02
*/
public class CsrfFilter implements Filter{ static Logger logger = Logger.getLogger(CsrfFilter.class); static Set<String> csrfUrls = new HashSet<String>(); static {
InputStream in = CsrfFilter.class.getResourceAsStream("/conf/csrf.properties");
Properties properties = new Properties();
try {
properties.load(in);
} catch (IOException e) {
e.printStackTrace();
}
Iterator<Entry<Object, Object>> it = properties.entrySet().iterator();
while (it.hasNext()) {
Entry<Object, Object> entry = it.next();
Object key = entry.getKey();
String keys = key.toString().trim();
String urlPref[] = keys.split("_");
String urlPrefix = "/";
for(String pu: urlPref){
urlPrefix += pu + "/";
}
logger.info("Prefix: " + urlPrefix);
Object value = entry.getValue();
String urls = value.toString();
String urlSuffix[] = urls.split(",");
String realUrl = "";
for(String suffix: urlSuffix){
suffix = suffix.trim();
realUrl = urlPrefix + suffix;
csrfUrls.add(realUrl);
logger.info("URL: " +
realUrl);
}
}

} /* (non-Javadoc)
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy() {
// TODO Auto-generated method stub } /* (non-Javadoc)
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
@Override
public void doFilter(ServletRequest req, ServletResponse rsp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest hReq = (HttpServletRequest) req;
Subject subject = SecurityUtils.getSubject();
boolean isAuthed = subject.isAuthenticated();
Session session = subject.getSession();
if(isAuthed) {
Object csrfToken = session.getAttribute(Constants.CURRENT_USER_JOB_KEY);
Object httpToken = hReq.getHeader(Constants.CURRENT_USER_JOB_KEY);
String uri = hReq.getRequestURI().toString();
String ctx = hReq.getContextPath().toString();
String tarUri = uri.substring(ctx.length(), uri.length());
logger.info("REQ URL:" + tarUri + ", sToken: " + csrfToken + ", rToken: " + httpToken);
if(csrfUrls.contains(tarUri)){
if (csrfToken != null && !csrfToken.equals(httpToken)){
Gson gson = new Gson();
ResultData<HashMap<String, String>> rd = new ResultData<HashMap<String, String>>();
rd.setSuccess(false);
rd.setMsg(Constants.CURRENT_CSRF_ERRINFO);
rsp.setCharacterEncoding("UTF-8");
rsp.setContentType("text/html;charset=UTF-8");
rsp.getWriter().write(gson.toJson(rd));
}else{
chain.doFilter(req, rsp);
}
}else{
chain.doFilter(req, rsp);
}
}else
{
chain.doFilter(req, rsp);
}

} /* (non-Javadoc)
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub } }

这里,配置文件在静态块里面加载,这里采用了一点点小技巧,方便配置简单化,因为一个后台系统配置功能页面,往往会有多个操作,例如:create,update,delete等,配置的时候,可以将key和value部分优化,然后后台加载时,进行URL路径组装重配。例如,我这里的配置文件:

#
#所有需要做CSRF拦截校验的URL,没有弄明白操作逻辑前,请勿修改
#Key部分是url组成的一部分,依据下划线分隔,和Value部分逗号分开的部分组合成最终的URL
#Value部分,反映的是一类业务中多个子类型的操作,每个都用逗号分隔
#例如:a_b=u1,u2 对应的URL信息解析后是: /a/b/u1和/a/b/u2
#
system_role=create,update,initUpdate,delete,saveRolePermission,addRolePermission
system_user=deleteUser,createUser,updateUser,userRole,saveUserRole

2》. 前端JS的核心代码

(function($){
var _ajax = $.ajax;
$.ajax = function(options){
var fn = {
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader(
"X-Job-Key", $("#csrfToken").val());
}

};
if(options.beforeSend){
fn.beforeSend = options.beforeSend;
}
var _options = $.extend(options,{
beforeSend: fn.beforeSend
})
_ajax(_options);
}
})(jQuery);

这段JS代码,是前端的核心,扩展了jQuery的ajax的行为,主要是将beforeSend函数扩展了,在每次只需ajax的时候,都要执行beforeSend,完成给Http请求头部添加一个自定义的属性值,供后台收到请求的时候,解析校验。

3. 注意事项

这里,主要涉及到几点,都是一些细节,容易落入坑里:

1》.前端用http头部自定义的属性,比较用Cookie安全,为了高效,通过扩展ajax的请求,就像我上面的核心代码JS部分的例子一样,每一个JS文件里面,类似上面加入这段代码。注意:代码最后有一个分号,这个分号一定得加上,否则,在一个JS文件里面,若有多个(function($){})(jQuery)这样的代码段,就会出现下面的错误。

Uncaught TypeError: (intermediate value)(intermediate value)(...) is not a function
at VM71 xxxx.js:

为了效率,beforeSend的使用,若只有少量的地方使用,可以采用下面的模式,在需要的ajax调用里面使用。

$.ajax({
url: url,
data: {"id":roleId},
dataType:"json",
//stype:"GET",
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("X-Job-Key", $("#randomx"
).val());
},

success: function(data){
if(data.isSuccess){
//初始化数据
initRoleData(data.object);
}else{
bootbox.alert(data.msg);
}
}
});

2》.Http头部定义的属性变量,不建议使用带有下划线的变量

这里,之所以这么说,主要是因为现在的web应用系统,很多会采用Nginx作为反向代理,Nginx会对Http请求头部的带有下划线的属性进行过滤处理,丢弃掉了。这样一来,带有下划线的属性,ajax或者其他模式发起的HTTP请求,就会被Nginx默认给丢弃了,后台应用服务器上,就获取不到该变量。

下面是Nginx官方文档对变量定义中下划线的描述:

underscores_in_headers

Context: http, server

Allows or disallows underscores in custom HTTP header names. If this directive
is set to on, the following example header is
considered valid by Nginx: test_
header: value.

Syntax: on or off
Default value: off

这个问题,被坑的现象是,在本地调试一点问题没有,上测试环境,就总是失败,获取rToken值总是null,才想起有这么一个坑。。。

上一篇:Codeforces Beta Round #97 (Div. 1) C. Zero-One 数学


下一篇:异常详细信息: System.ComponentModel.Win32Exception: 拒绝访问。