springboot中使用过滤器,jsoup过滤XSS脚本详解
springboot使用过滤器,jsoup过滤xss脚本
背景:略
目标:完成request请求中的脚本过滤
技术:filter,jsoup,requestwapper
1.把可能包含脚本的参数位置分析一下
-
post
/put
/delete
: 请求的参数中,有可能是表单提交、也有可能是使用了@requestbody注解,那么参数就是json格式,位于request的流中。 -
get
/options
等:可能存在于url参数中,也有可能是表单提交的预请求中,所以一般在能想到的位置都有可能存在,包括header中。
2.分析实现过程
2.1首先要从request请求中将各个需要过滤位置的参数取出来
2.2然后将参数取出来进行过滤
2.3将过滤后的参数重新包装成request传递下去
2.4在这期间,
- 需要准备好jsoup过滤脚本的工具类;
- 需要自定义一个过滤器,并且在过滤器中添加匹配条件,如:那些url不需要过滤,那些请求方式必须进行过滤;
- 对过滤器进行配置,是否开启,设置在整个过滤器链中的位置,设置过滤的白名单或者黑名单
- 所以就很清晰了我们过滤需要哪些类,哪些配置了
一个filter
一个requestwapper
一个jsoup工具类
一个filter的配置类
2.5进行数据测试
3.代码实现过程
3.1.jsoup依赖:
<!--screen xss --> <dependency> <groupid>org.jsoup</groupid> <artifactid>jsoup</artifactid> <version>1.9.2</version> </dependency>
3.2jsoup工具类:jsouputil
import org.jsoup.jsoup; import org.jsoup.nodes.document; import org.jsoup.safety.whitelist; import java.io.filenotfoundexception; import java.io.ioexception; /** * @auther: qianshanmuxue * @date: 2019/2/27 19:32 * @description: xss illegal label filtering */ public class jsouputil { private static final whitelist whitelist = whitelist.simpletext();//jsoup白名单种类,有四种,每一种针对的标签类型不一样,具体的可以ctrl+左键点击simpletext,在jsoup源码中有响应的注释和标签名单 //add myself white list label private static final document.outputsettings outputsettings = new document.outputsettings().prettyprint(false); static { whitelist.addattributes(":all", "style").addtags("p").addtags("strong");//将自定义标签添加进白名单,除开白名单之外的标签都会被过滤 whitelist.preserverelativelinks(true);//这个配置的意思的过滤如果找不到成对的标签,就只过滤单个标签,而不用把后面所有的文本都进行过滤。 //(之前在这个问题上折腾了很久,当<script>标签只有一个时,会<script>标签后面的数据全部过滤) } public static string clean(string content) { //过滤方法 return jsoup.clean(content, "", whitelist, outputsettings); } //test main public static void main(string[] args) throws filenotfoundexception, ioexception { string text = "<a href=\"http://www.baidu.com/a\" onclick=\"alert(1);\"><strong><p>sss</p></strong></a><script>alert(0);</script>sss"; system.out.println(clean(text)); } }
3.3request包装类xsshttpservletrequestwrapper
import java.io.*; import java.util.*; import javax.servlet.readlistener; import javax.servlet.servletinputstream; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletrequestwrapper; import com.xxx.utils.jsouputil; import org.jsoup.nodes.document; import org.springframework.util.stringutils; /** * @auther: qianshanmuxue * @date: 2019/2/27 16:24 * @description:request wapper use to get request parameter and request bdoy data and wapper another request */ public class xsshttpservletrequestwrapper extends httpservletrequestwrapper { //因为我们需要获取request中的数据,所以需要继承java底层中httpservletrequestwrapper这个类,重写父类中的某些方法,获取相应位置的参数 private httpservletrequest orgrequest = null; private static final document.outputsettings outputsettings = new document.outputsettings().prettyprint(false); public xsshttpservletrequestwrapper(httpservletrequest request) { super(request); orgrequest = request; } @override public servletinputstream getinputstream() throws ioexception {//get bufferedreader br = new bufferedreader(new inputstreamreader(orgrequest.getinputstream())); string line = br.readline(); string result = ""; if (line != null) { result += clean(line); } return new wrappedservletinputstream(new bytearrayinputstream(result.getbytes())); } @override public string getparameter(string name) { if (("content".equals(name) || name.endswith("withhtml"))) { return super.getparameter(name); } name = clean(name); string value = super.getparameter(name); if (!stringutils.isempty(value)) { value = clean(value); } return value; } @override public map getparametermap() { map map = super.getparametermap(); // 返回值map map<string, string> returnmap = new hashmap<string, string>(); iterator entries = map.entryset().iterator(); map.entry entry; string name = ""; string value = ""; while (entries.hasnext()) { entry = (map.entry) entries.next(); name = (string) entry.getkey(); object valueobj = entry.getvalue(); if (null == valueobj) { value = ""; } else if (valueobj instanceof string[]) { string[] values = (string[]) valueobj; for (int i = 0; i < values.length; i++) { value = values[i] + ","; } value = value.substring(0, value.length() - 1); } else { value = valueobj.tostring(); } returnmap.put(name, clean(value).trim()); } return returnmap; } @override public string[] getparametervalues(string name) { string[] arr = super.getparametervalues(name); if (arr != null) { for (int i = 0; i < arr.length; i++) { arr[i] = clean(arr[i]); } } return arr; } /** * get org request * * @return */ public httpservletrequest getorgrequest() { return orgrequest; } /** * wapper request */ public static httpservletrequest getorgrequest(httpservletrequest req) { if (req instanceof xsshttpservletrequestwrapper) { return ((xsshttpservletrequestwrapper) req).getorgrequest(); } return req; } public string clean(string content) { string result = jsouputil.clean(content); return result; } private class wrappedservletinputstream extends servletinputstream { public void setstream(inputstream stream) { this.stream = stream; } private inputstream stream; public wrappedservletinputstream(inputstream stream) { this.stream = stream; } @override public int read() throws ioexception { return stream.read(); } @override public boolean isfinished() { return true; } @override public boolean isready() { return true; } @override public void setreadlistener(readlistener readlistener) { } } }
3.4filter-xssfilter
import org.apache.commons.lang.booleanutils; import org.apache.commons.lang.stringutils; import java.io.ioexception; import java.util.arraylist; import java.util.list; import java.util.regex.matcher; import java.util.regex.pattern; 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; /** * @auther: qianshanmuxue * @date: 2019/2/27 16:25 * @description: */ //@webfilter //@component 在这里可以不用这个注解,以为后面我们会在config中去配置这个filter,在这里只需要实现 filter 接口实现相应的方法就ok public class xssfilter implements filter { private static boolean is_include_rich_text = false;//用于接收配置中的参数,决定这个过滤器是否开启 public list<string> excludes = new arraylist<string>();//用于接收配置中的参数,决定哪些是不需要过滤的url(在这里,也可以修改handleexcludeurl()方法中相应的代码,使其变更为只需要过滤的url) @override public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { httpservletrequest req = (httpservletrequest) request; httpservletresponse resp = (httpservletresponse) response; if (handleexcludeurl(req, resp)) { chain.dofilter(request, response); return; } xsshttpservletrequestwrapper xssrequest = new xsshttpservletrequestwrapper((httpservletrequest) request); chain.dofilter(xssrequest, response); } /** *此方法是决定对当前url是否执行过滤, *在这里没有使用请求方法(post/put)来匹配,因为在本项目中使用url匹配更适合(因为get和其他请求方式也需要进行过滤),如果你有兴趣可以把这个方法更改为匹配请求方法进行过滤 **/ private boolean handleexcludeurl(httpservletrequest request, httpservletresponse response) { if ((excludes == null || excludes.isempty())&&is_include_rich_text) { return false; } string url = request.getservletpath(); for (string pattern : excludes) { pattern p = pattern.compile("^" + pattern); matcher m = p.matcher(url); if (m.find()) { return true; } } return false; } /** *过滤器初始化,从配置类中获取参数,用于初始化两个参数(是否开启,排除指定的url list) * */ @override public void init(filterconfig arg0) throws servletexception { string isincluderichtext = arg0.getinitparameter("isincluderichtext"); if (stringutils.isnotblank(isincluderichtext)) { is_include_rich_text = booleanutils.toboolean(isincluderichtext); } string temp = arg0.getinitparameter("excludes"); if (temp != null) { string[] url = temp.split(","); for (int i = 0; url != null && i < url.length; i++) { excludes.add(url[i]); } } } @override public void destroy() { } }
3.5filter的配置类:xssconfig
import com.xxx.filter.xssfilter; import com.google.common.collect.maps; import org.springframework.boot.web.servlet.filterregistrationbean; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import java.util.map; /** * @auther: qianshanmuxue * @date: 2019/2/27 16:49 * @description: xss filter config */ @configuration public class xssconfig { @bean public filterregistrationbean xssfilterregistrationbean() { filterregistrationbean filterregistrationbean = new filterregistrationbean(); filterregistrationbean.setfilter(new xssfilter()); filterregistrationbean.setorder(1);//filter order ,set it first filterregistrationbean.setenabled(true); filterregistrationbean.addurlpatterns("/*"); //set filter all url mapping map<string, string> initparameters = maps.newhashmap(); initparameters.put("excludes", "/oauth/token");///white list url initparameters.put("isincluderichtext", "true");//enable or disable filterregistrationbean.setinitparameters(initparameters); return filterregistrationbean; } }
调试截图:
请求:
程序截图:
运行结果:
可以看到body中 的脚本已经被过滤了,
然后其他的截图我就不发了,还有一种思路就是在过滤器中把字符转义。
感谢luckpet大佬的提示
1 bufferedreader 使用完需要关闭 ;
2 对于一些拿postman等工具的朋友,拼接json的话会有换行 这里result += clean(line); 需要改成: while((line = br.readline()) != null){ if (line != null) { result += line; } }
使用jsoup防止xss攻击
前阵子项目国测后,打开一个项目页面,莫名其妙弹出xss,搜了全局也没找到alert("xss"),问了一下项目经理,原来是国测做防注入的时候,在添加数据的时候做的,一脸懵逼。
查了一下资料,以前做项目的时候都没想到这个问题,如果保存一段script脚本,查数据的时候,这段脚本就会被执行,这东西后果挺严重啊,如果是在桌面外弹框,执行个挖矿脚本,这玩意不得了啊,厉害,长知识了。。。
<dependency> <groupid>org.jsoup</groupid> <artifactid>jsoup</artifactid> <version>1.11.3</version> </dependency>
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。