后端應(yīng)用經(jīng)常接收各種信息參數(shù),例如評(píng)論,回復(fù)等文本內(nèi)容蛤售。除了一些場(chǎng)景下面,可以特定接受的富文本標(biāo)簽和屬性之外(如:b,ul,li,h1, h2, h3...),需要過(guò)濾掉危險(xiǎn)的字符和標(biāo)簽,防止xss攻擊悴能。
一揣钦、什么是XSS?
看完這個(gè),應(yīng)該有一個(gè)大致的概念漠酿。
二寻狂、準(zhǔn)則
- 永遠(yuǎn)不要相信用戶的輸入和請(qǐng)求的參數(shù)(包括文字眼刃、上傳等一切內(nèi)容)
- 參考第1條
三、實(shí)現(xiàn)做法
結(jié)合具體業(yè)務(wù)場(chǎng)景,對(duì)相應(yīng)內(nèi)容進(jìn)行過(guò)濾图呢,這里使用Jsoup褐桌。
jsoup是一款Java的HTML解析器照雁。Jsoup提供的Whitelist(白名單)對(duì)文本內(nèi)容進(jìn)行過(guò)濾秋泄,過(guò)濾掉字符、屬性夭拌,但是又保留必要的富文本格式呀洲。
如,白名單中允許b標(biāo)簽存在(并且不允許b標(biāo)簽帶有其他屬性)那么在一段Html內(nèi)容啼止,在過(guò)濾之后道逗,會(huì)變成:
過(guò)濾前:
<b style="xxx" onclick="<script>alert(0);</script>">abc</>
過(guò)濾后:
<b>abc</b>
Whitelist主要方法說(shuō)明
方法 | 說(shuō)明 |
---|---|
addAttributes(String tag, String... attributes) | 給標(biāo)簽添加屬性。Tag是屬性名献烦,keys對(duì)應(yīng)的是一個(gè)個(gè)屬性值滓窍。例如:addAttributes("a", "href", "class")表示:給標(biāo)簽a添加href和class屬性,即允許標(biāo)簽a包含href和class屬性巩那。如果想給每一個(gè)標(biāo)簽添加一組屬性吏夯,使用:all。例如:addAttributes(":all", "class").即給每個(gè)標(biāo)簽添加class屬性即横。 |
addEnforcedAttribute(String tag, String attribute, String value) | 給標(biāo)簽添加強(qiáng)制性屬性噪生,如果標(biāo)簽已經(jīng)存在了要添加的屬性,則覆蓋原有值东囚。tag:標(biāo)簽跺嗽;key:標(biāo)簽的鍵;value:標(biāo)簽的鍵對(duì)應(yīng)的值页藻。例如:addEnforcedAttribute("a", "rel", "nofollow")表示<a href="..." rel="nofollow"> |
addProtocols(String tag, String key, String...protocols) | 給URL屬性添加協(xié)議桨嫁。例如:addProtocols("a", "href", "ftp", "http", "https")標(biāo)簽a的href鍵可以指向的協(xié)議有ftp、http份帐、https |
addTags(String... tags) | 向Whitelist添加標(biāo)簽 |
basic() | 允許的標(biāo)簽包括: a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, strike, strong, sub, sup, u, ul,以及合適的屬性璃吧。標(biāo)簽a指向的連接可以是 http, https, ftp, mailto,轉(zhuǎn)換完后會(huì)強(qiáng)制添加 rel=nofollow這個(gè)屬性废境。不允許包含圖片畜挨。 |
basicWithImages() | 在basic的基礎(chǔ)上增加了圖片的標(biāo)簽:img以及使用src指向http或https類型的圖片鏈接筒繁。 |
none() | 只保留文本,其他所有的html內(nèi)容均被刪除 |
preserveRelativeLinks(booleanpreserve) | false(默認(rèn)):不保留相對(duì)地址的url巴元;true:保留相對(duì)地址的url |
relaxed() | 允許的標(biāo)簽: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毡咏。結(jié)果不包含標(biāo)簽rel=nofollow,如果需要可以手動(dòng)添加务冕。 |
simpleText() | 只允許:b, em, i, strong, u。 |
四幻赚、例子
基于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過(guò)濾類
package net.lofish.xpra.xss;
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 擼小魚(yú)
* Created by lofish@foxmail.com on 2020-04-12
*/
public class HtmlFilter {
/**
* 默認(rèn)使用relaxed()
* 允許的標(biāo)簽: 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禀忆。結(jié)果不包含標(biāo)簽rel=nofollow ,如果需要可以手動(dòng)添加落恼。
*/
private Whitelist whiteList;
/**
* 配置過(guò)濾化參數(shù),不對(duì)代碼進(jìn)行格式化
*/
private Document.OutputSettings outputSettings;
private HtmlFilter() {
}
/**
* 靜態(tài)創(chuàng)建HtmlFilter方法
* @param whiteList 白名單標(biāo)簽
* @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;
}
/**
* 靜態(tài)創(chuàng)建HtmlFilter方法
* @return HtmlFilter
*/
public static HtmlFilter create() {
return create(null, false);
}
/**
* 靜態(tài)創(chuàng)建HtmlFilter方法
* @param whiteList 白名單標(biāo)簽
* @return HtmlFilter
*/
public static HtmlFilter create(Whitelist whiteList) {
return create(whiteList, false);
}
/**
* 靜態(tài)創(chuàng)建HtmlFilter方法
* @param excludeTags 例外的特定標(biāo)簽
* @param includeTags 需要過(guò)濾的特定標(biāo)簽
* @param pretty 是否格式化
* @return HtmlFilter
*/
public static HtmlFilter create( List<String> excludeTags,List<String> includeTags, boolean pretty) {
HtmlFilter filter = create(null, pretty);
//要過(guò)濾的標(biāo)簽
if (includeTags != null && !includeTags.isEmpty()) {
String[] tags = (String[]) includeTags.toArray(new String[0]);
filter.whiteList.removeTags(tags);
}
//例外標(biāo)簽
if (excludeTags != null && !excludeTags.isEmpty()) {
String[] tags = (String[]) excludeTags.toArray(new String[0]);
filter.whiteList.addTags(tags);
}
return filter;
}
/**
* 靜態(tài)創(chuàng)建HtmlFilter方法
* @param excludeTags 例外的特定標(biāo)簽
* @param includeTags 需要過(guò)濾的特定標(biāo)簽
* @return HtmlFilter
*/
public static HtmlFilter create(List<String> excludeTags,List<String> includeTags) {
return create( includeTags, excludeTags, false );
}
/**
* @param content 需要過(guò)濾內(nèi)容
* @return 過(guò)濾后的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過(guò)濾器
package net.lofish.xpra.xss;
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 擼小魚(yú)
* Created by lofish@foxmail.com on 2020-04-12
*/
public class XssFilter implements Filter {
/**
* 例外urls
*/
private List<String> excludeUrls = new ArrayList<>();
/**
* 例外標(biāo)簽
*/
private List<String> excludeTags = new ArrayList<>();
/**
* 需要過(guò)濾標(biāo)簽
*/
private List<String> includeTags = new ArrayList<>();
/**
* 開(kāi)關(guān)
*/
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("excludeUrls");
String excludeTagStr = filterConfig.getInitParameter("excludeTagStr");
String includeTagStr = filterConfig.getInitParameter("excludeTagStr");
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(excludeTagStr)) {
String[] url = excludeTagStr.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;
}
}
一般情況下箩退,我們都是通過(guò)request的parameter來(lái)傳遞參數(shù)。
但是佳谦,如果在某些場(chǎng)景下面戴涝,通過(guò)requestBody體(json等),來(lái)傳遞相應(yīng)參數(shù)應(yīng)該怎么辦钻蔑?
這就要需要我們對(duì)request的inputStream來(lái)進(jìn)行來(lái)過(guò)濾處理了
有個(gè)地方需要注意一下的:
servlet中inputStream只能一次讀取啥刻,后續(xù)不能再次讀取inputStream。Xss過(guò)濾器中讀取了stream之后咪笑,后續(xù)如果其他邏輯涉及到inputStream讀取可帽,會(huì)拋出異常。那我們就需要想辦法把已經(jīng)讀取的stream窗怒,重新放回到請(qǐng)求中映跟。
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過(guò)濾處理
* @author 擼小魚(yú)
* 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 例外的特定標(biāo)簽
* @param includeTags 需要過(guò)濾的標(biāo)簽
*/
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過(guò)濾
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只能一次讀取,后續(xù)不能再次讀取inputStream
* xss過(guò)濾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中注冊(cè)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;
}
}
測(cè)試
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