使用Jsoup防御XSS攻击
使用Jsoup防御XSS攻击
跨站脚本攻击(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
使用Jsoup可以有效的过滤不安全的代码。Jsoup使用白名单的机制来预防XSS攻击,比如白名单中规定只允许<span>
标签的存在,那么其他标签都会被过滤掉。
常见的XSS攻击
比如页面的某个表单允许用户输入任意内容,当某个调皮的用户输入如下内容:
保存后,你会发现页面文字都变成了红色!
或者输入<script>for(var i=0;i<10;i++){alert("fuck you");}</script>
,保存后页面将弹窗10次!
引入Jsoup
使用Maven构建一个简单的Spring Boot项目,在pom中引入:
1 2 3 4 5 |
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.9.2</version> </dependency> |
JsoupUtil
创建一个JsoupUtil工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import java.io.FileNotFoundException; import java.io.IOException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.safety.Whitelist; /** * Xss过滤工具 * */ public class JsoupUtil { private static final Whitelist whitelist = Whitelist.basicWithImages(); /* * 配置过滤化参数,不对代码进行格式化 */ private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); static { /* * 富文本编辑时一些样式是使用style来进行实现的 比如红色字体 style="color:red;" 所以需要给所有标签添加style属性 */ whitelist.addAttributes(":all", "style"); } public static String clean(String content) { return Jsoup.clean(content, "", whitelist, outputSettings); } } |
这里采用的白名单为basicWithImages
,Jsoup内置了几种常见的白名单供我们选择,如下表所示:
白名单对象 | 标签 | 说明 |
---|---|---|
none | 无 | 只保留标签内文本内容 |
simpleText | b,em,i,strong,u | 简单的文本标签 |
basic | a,b,blockquote,br,cite,code,dd, dl,dt,em,i,li,ol,p,pre,q,small,span, strike,strong,sub,sup,u,ul |
基本使用的标签 |
basicWithImages | basic 的基础上添加了 img 标签 及 img 标签的 src,align,alt,height,width,title 属性 |
基本使用的加上 img 标签 |
relaxed | a,b,blockquote,br,caption,cite, code,col,colgroup,dd,div,dl,dt, em,h1,h2,h3,h4,h5,h6,i,img,li, ol,p,pre,q,small,span,strike,strong, sub,sup,table,tbody,td,tfoot,th,thead,tr,u,ul |
在 basicWithImages 的基础上又增加了一部分部分标签 |
XssHttpServletRequestWrapper
创建一个XssHttpServletRequestWrapper,同过重写getParameter()
,getParameterValues()
和getHeader()
方法来过滤HTTP请求中参数包含的恶意字符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.apache.commons.lang.StringUtils; import cc.mrbird.common.util.JsoupUtil; /** * Jsoup过滤http请求,防止Xss攻击 * */ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { HttpServletRequest orgRequest = null; private boolean isIncludeRichText = false; public XssHttpServletRequestWrapper(HttpServletRequest request, boolean isIncludeRichText) { super(request); orgRequest = request; this.isIncludeRichText = isIncludeRichText; } /** * 覆盖getParameter方法,将参数名和参数值都做xss过滤如果需要获得原始的值,则通过super.getParameterValues(name)来获取 * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖 */ @Override public String getParameter(String name) { if (("content".equals(name) || name.endsWith("WithHtml")) && !isIncludeRichText) { return super.getParameter(name); } name = JsoupUtil.clean(name); String value = super.getParameter(name); if (StringUtils.isNotBlank(value)) { value = JsoupUtil.clean(value); } return value; } @Override public String[] getParameterValues(String name) { String[] arr = super.getParameterValues(name); if (arr != null) { for (int i = 0; i < arr.length; i++) { arr[i] = JsoupUtil.clean(arr[i]); } } return arr; } /** * 覆盖getHeader方法,将参数名和参数值都做xss过滤如果需要获得原始的值,则通过super.getHeaders(name)来获取 * getHeaderNames 也可能需要覆盖 */ @Override public String getHeader(String name) { name = JsoupUtil.clean(name); String value = super.getHeader(name); if (StringUtils.isNotBlank(value)) { value = JsoupUtil.clean(value); } return value; } /** * 获取原始的request */ public HttpServletRequest getOrgRequest() { return orgRequest; } /** * 获取原始的request的静态方法 */ public static HttpServletRequest getOrgRequest(HttpServletRequest req) { if (req instanceof XssHttpServletRequestWrapper) { return ((XssHttpServletRequestWrapper) req).getOrgRequest(); } return req; } } |
XssFilter
创建XssFilter,同过使用上面定义的XssHttpServletRequestWrapper类中的getParameter()
等方法来保证参数得到了过滤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
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; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Xss攻击拦截器 * */ public class XssFilter implements Filter { private static Logger logger = LoggerFactory.getLogger(XssFilter.class); // 是否过滤富文本内容 private static boolean IS_INCLUDE_RICH_TEXT = false; public List<String> excludes = new ArrayList<String>(); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("------------ xss filter init ------------"); String isIncludeRichText = filterConfig.getInitParameter("isIncludeRichText"); if (StringUtils.isNotBlank(isIncludeRichText)) { IS_INCLUDE_RICH_TEXT = BooleanUtils.toBoolean(isIncludeRichText); } String temp = filterConfig.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 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, IS_INCLUDE_RICH_TEXT); chain.doFilter(xssRequest, response); } @Override public void destroy() { } private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) { if (excludes == null || excludes.isEmpty()) { 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; } } |
Spring Boot中配置XssFilter
使用JavaConfig的形式配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Bean public FilterRegistrationBean xssFilterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new XssFilter()); filterRegistrationBean.setOrder(1); filterRegistrationBean.setEnabled(true); filterRegistrationBean.addUrlPatterns("/*"); Map<String, String> initParameters = new HashMap<String, String>(); initParameters.put("excludes", "/favicon.ico,/img/*,/js/*,/css/*"); initParameters.put("isIncludeRichText", "true"); filterRegistrationBean.setInitParameters(initParameters); return filterRegistrationBean; } |
参考文章: