后端應用經常接收各種信息參數,例如評論,回復等文本內容。除了一些場景下面,可以特定接受的富文本標簽和屬性之外(如:b,ul,li,h1, h2, h3...),需要過濾掉危險的字符和標簽,防止xss攻擊。
一、什么是XSS?
看完這個,應該有一個大致的概念。
二、准則
- 永遠不要相信用戶的輸入和請求的參數(包括文字、上傳等一切內容)
- 參考第1條
三、實現做法
結合具體業務場景,對相應內容進行過濾,這里使用Jsoup。
jsoup是一款Java的HTML解析器。Jsoup提供的Whitelist(白名單)對文本內容進行過濾,過濾掉字符、屬性,但是又保留必要的富文本格式。
如,白名單中允許b標簽存在(並且不允許b標簽帶有其他屬性)那么在一段Html內容,在過濾之后,會變成:
過濾前:
<b style="xxx" onclick="<script>alert(0);</script>">abc</>
過濾后:
<b>abc</b>
Whitelist主要方法說明
四、例子
基於springboot
pom.xml依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
HtmlFilter過濾類
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
/**
* HtmlFilter
*
* @author 擼小魚
* Created by lofish@foxmail.com on 2020-04-12
*/
public class HtmlFilter {
/**
* 默認使用relaxed()
* 允許的標簽: a, b, blockquote, br, caption, cite, code, col, colgroup, dd, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, strike, strong, sub, sup, table, tbody, td, tfoot, th, thead, tr, u, ul。結果不包含標簽rel=nofollow ,如果需要可以手動添加。
*/
private Whitelist whiteList;
/**
* 配置過濾化參數,不對代碼進行格式化
*/
private Document.OutputSettings outputSettings;
private HtmlFilter() {
}
/**
* 靜態創建HtmlFilter方法
* @param whiteList 白名單標簽
* @param pretty 是否格式化
* @return HtmlFilter
*/
public static HtmlFilter create(Whitelist whiteList, boolean pretty) {
HtmlFilter filter = new HtmlFilter();
if (whiteList == null) {
filter.whiteList = Whitelist.relaxed();
}
filter.outputSettings = new Document.OutputSettings().prettyPrint(pretty);
return filter;
}
/**
* 靜態創建HtmlFilter方法
* @return HtmlFilter
*/
public static HtmlFilter create() {
return create(null, false);
}
/**
* 靜態創建HtmlFilter方法
* @param whiteList 白名單標簽
* @return HtmlFilter
*/
public static HtmlFilter create(Whitelist whiteList) {
return create(whiteList, false);
}
/**
* 靜態創建HtmlFilter方法
* @param excludeTags 例外的特定標簽
* @param includeTags 需要過濾的特定標簽
* @param pretty 是否格式化
* @return HtmlFilter
*/
public static HtmlFilter create( List<String> excludeTags,List<String> includeTags, boolean pretty) {
HtmlFilter filter = create(null, pretty);
//要過濾的標簽
if (includeTags != null && !includeTags.isEmpty()) {
String[] tags = (String[]) includeTags.toArray(new String[0]);
filter.whiteList.removeTags(tags);
}
//例外標簽
if (excludeTags != null && !excludeTags.isEmpty()) {
String[] tags = (String[]) excludeTags.toArray(new String[0]);
filter.whiteList.addTags(tags);
}
return filter;
}
/**
* 靜態創建HtmlFilter方法
* @param excludeTags 例外的特定標簽
* @param includeTags 需要過濾的特定標簽
* @return HtmlFilter
*/
public static HtmlFilter create(List<String> excludeTags,List<String> includeTags) {
return create( includeTags, excludeTags, false );
}
/**
* @param content 需要過濾內容
* @return 過濾后的String
*/
public String clean(String content) {
return Jsoup.clean(content, "", this.whiteList, this.outputSettings);
}
public static void main(String[] args) throws FileNotFoundException, IOException {
String text = "<a href=\"http://www.baidu.com/a\" onclick=\"alert(1);\"></a><script>alert(0);</script><b style=\"xxx\" onclick=\"<script>alert(0);</script>\">abc</>";
System.out.println(HtmlFilter.create().clean(text));
}
}
XssFilter過濾器
import org.apache.commons.lang3.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* XssFilter
*
* @author 擼小魚
* Created by lofish@foxmail.com on 2020-04-12
*/
public class XssFilter implements Filter {
/**
* 例外urls
*/
private List<String> excludeUrls = new ArrayList<>();
/**
* 例外標簽
*/
private List<String> excludeTags = new ArrayList<>();
/**
* 需要過濾標簽
*/
private List<String> includeTags = new ArrayList<>();
/**
* 開關
*/
public boolean enabled = false;
/**
* 編碼
*/
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String enabledStr = filterConfig.getInitParameter("enabled");
String excludeUrlStr = filterConfig.getInitParameter("urlPatterns");
String excludeTagStr = filterConfig.getInitParameter("excludes");
String includeTagStr = filterConfig.getInitParameter("includes");
String encodingStr = filterConfig.getInitParameter("encoding");
if (StringUtils.isNotEmpty(excludeUrlStr)) {
String[] url = excludeUrlStr.split(",");
Collections.addAll(this.excludeUrls, url);
}
if (StringUtils.isNotEmpty(excludeTagStr)) {
String[] url = excludeTagStr.split(",");
Collections.addAll(this.excludeTags, url);
}
if (StringUtils.isNotEmpty(includeTagStr)) {
String[] url = includeTagStr.split(",");
Collections.addAll(this.includeTags, url);
}
if (StringUtils.isNotEmpty(enabledStr)) {
this.enabled = Boolean.parseBoolean(enabledStr);
}
if (StringUtils.isNotEmpty(encodingStr)) {
this.encoding = encodingStr;
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (handleExcludeUrls(req, resp)) {
chain.doFilter(request, response);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request, encoding, excludeTags, includeTags );
chain.doFilter(xssRequest, response);
}
private boolean handleExcludeUrls(HttpServletRequest request, HttpServletResponse response) {
if (!enabled) {
return true;
}
if (excludeUrls == null || excludeUrls.isEmpty()) {
return false;
}
String url = request.getServletPath();
for (String pattern : excludeUrls) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}
}
一般情況下,我們都是通過request的parameter來傳遞參數。
但是,如果在某些場景下面,通過requestBody體(json等),來傳遞相應參數應該怎么辦?
這就要需要我們對request的inputStream來進行來過濾處理了
有個地方需要注意一下的:
servlet中inputStream只能一次讀取,后續不能再次讀取inputStream。Xss過濾器中讀取了stream之后,后續如果其他邏輯涉及到inputStream讀取,會拋出異常。那我們就需要想辦法把已經讀取的stream,重新放回到請求中。
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* XSS過濾處理
* @author 擼小魚
* Created by lofish@foxmail.com
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper{
HttpServletRequest orgRequest;
String encoding;
HtmlFilter htmlFilter;
private final static String JSON_CONTENT_TYPE = "application/json";
private final static String CONTENT_TYPE = "Content-Type";
/**
* @param request HttpServletRequest
* @param encoding 編碼
* @param excludeTags 例外的特定標簽
* @param includeTags 需要過濾的標簽
*/
public XssHttpServletRequestWrapper( HttpServletRequest request, String encoding, List<String> excludeTags, List<String> includeTags ){
super( request );
orgRequest = request;
this.encoding = encoding;
this.htmlFilter = HtmlFilter.create( excludeTags, includeTags );
}
/**
*
* @param request HttpServletRequest
* @param encoding 編碼
*/
public XssHttpServletRequestWrapper( HttpServletRequest request, String encoding ){
this( request, encoding, null, null );
}
private String xssFilter( String input ){
return htmlFilter.clean( input );
}
@Override
public ServletInputStream getInputStream() throws IOException{
// 非json處理
if( !JSON_CONTENT_TYPE.equalsIgnoreCase( super.getHeader( CONTENT_TYPE ) ) ){
return super.getInputStream();
}
InputStream in = super.getInputStream();
String body = IOUtils.toString( in, encoding );
IOUtils.closeQuietly( in );
//空串處理直接返回
if( StringUtils.isBlank( body ) ){
return super.getInputStream();
}
// xss過濾
body = xssFilter( body );
return new RequestCachingInputStream( body.getBytes( encoding ) );
}
@Override
public String getParameter( String name ){
String value = super.getParameter( xssFilter( name ) );
if( StringUtils.isNotBlank( value ) ){
value = xssFilter( value );
}
return value;
}
@Override
public String[] getParameterValues( String name ){
String[] parameters = super.getParameterValues( name );
if( parameters == null || parameters.length == 0 ){
return null;
}
for( int i = 0; i < parameters.length; i++ ){
parameters[i] = xssFilter( parameters[i] );
}
return parameters;
}
@Override
public Map<String, String[]> getParameterMap(){
Map<String, String[]> map = new LinkedHashMap<>();
Map<String, String[]> parameters = super.getParameterMap();
for( String key : parameters.keySet() ){
String[] values = parameters.get( key );
for( int i = 0; i < values.length; i++ ){
values[i] = xssFilter( values[i] );
}
map.put( key, values );
}
return map;
}
@Override
public String getHeader( String name ){
String value = super.getHeader( xssFilter( name ) );
if( StringUtils.isNotBlank( value ) ){
value = xssFilter( value );
}
return value;
}
/**
* <b>
* #獲取最原始的request
* </b>
*/
public HttpServletRequest getOrgRequest(){
return orgRequest;
}
/**
* <b>
* #獲取最原始的request
* </b>
* @param request HttpServletRequest
*/
public static HttpServletRequest getOrgRequest( HttpServletRequest request ){
if( request instanceof XssHttpServletRequestWrapper ){
return ((XssHttpServletRequestWrapper) request).getOrgRequest();
}
return request;
}
/**
* <pre>
* servlet中inputStream只能一次讀取,后續不能再次讀取inputStream
* xss過濾body后,重新把流放入ServletInputStream中
* </pre>
*/
private static class RequestCachingInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public RequestCachingInputStream(byte[] bytes) {
inputStream = new ByteArrayInputStream(bytes);
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener( ReadListener readListener ){
}
}
}
springboot2.2.4.RELEASE中注冊Filter
@Configuration
public class XssFilterConfig {
@Value("${xss.enabled:true}")
private String enabled;
@Value("${xss.excludes:}")
private String excludes;
@Value("${xss.includes$:}")
private String includes;
@Value("${xss.urlPatterns:/*}")
private String urlPatterns;
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistrationBean() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns(urlPatterns.split(","));
registration.setName("XssFilter");
registration.setOrder(Integer.MAX_VALUE);
Map<String, String> initParameters = new HashMap<String, String>();
initParameters.put("excludes", excludes);
initParameters.put("includes", excludes);
initParameters.put("enabled", enabled);
registration.setInitParameters(initParameters);
return registration;
}
}
測試
http://localhost:8080/demo/th/xss?abc=%3Ca%20href=%22http://www.baidu.com/a%22%20onclick=%22alert(1);%22%3Eabc%3C/a%3E%3Cscript%3Ealert(0);%3C/script%3E&abc=%3Cb%20style=%22xxx%22%20onclick=%22%3Cscript%3Ealert(0);%3C/script%3E%22%3Eabc%3C/%3E