springboot2.x使用Jsoup防XSS攻擊的實現(xiàn)
后端應用經常接收各種信息參數(shù),例如評論,回復等文本內容。除了一些場景下面,可以特定接受的富文本標簽和屬性之外(如:b,ul,li,h1, h2, h3...),需要過濾掉危險的字符和標簽,防止xss攻擊。
一、什么是XSS?
看完這個,應該有一個大致的概念。
二、準則
- 永遠不要相信用戶的輸入和請求的參數(shù)(包括文字、上傳等一切內容)
- 參考第1條
三、實現(xiàn)做法
結合具體業(yè)務場景,對相應內容進行過濾,這里使用Jsoup。
jsoup是一款Java的HTML解析器。Jsoup提供的Whitelist(白名單)對文本內容進行過濾,過濾掉字符、屬性,但是又保留必要的富文本格式。
如,白名單中允許b標簽存在(并且不允許b標簽帶有其他屬性)那么在一段Html內容,在過濾之后,會變成:
過濾前:
<b style="xxx" onclick="<script>alert(0);</script>">abc</>
過濾后:
<b>abc</b>
Whitelist主要方法說明
方法 | 說明 |
---|---|
addAttributes(String tag, String... attributes) | 給標簽添加屬性。Tag是屬性名,keys對應的是一個個屬性值。例如:addAttributes("a", "href", "class")表示:給標簽a添加href和class屬性,即允許標簽a包含href和class屬性。如果想給每一個標簽添加一組屬性,使用:all。例如:addAttributes(":all", "class").即給每個標簽添加class屬性。 |
addEnforcedAttribute(String tag, String attribute, String value) | 給標簽添加強制性屬性,如果標簽已經存在了要添加的屬性,則覆蓋原有值。tag:標簽;key:標簽的鍵;value:標簽的鍵對應的值。例如:addEnforcedAttribute("a", "rel", "nofollow")表示 |
addProtocols(String tag, String key, String...protocols) | 給URL屬性添加協(xié)議。例如:addProtocols("a", "href", "ftp", "http", "https")標簽a的href鍵可以指向的協(xié)議有ftp、http、https |
addTags(String... tags) | 向Whitelist添加標簽 |
basic() | 允許的標簽包括: a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, strike, strong, sub, sup, u, ul,以及合適的屬性。標簽a指向的連接可以是 http, https, ftp, mailto,轉換完后會強制添加 rel=nofollow這個屬性。不允許包含圖片。 |
basicWithImages() | 在basic的基礎上增加了圖片的標簽:img以及使用src指向http或https類型的圖片鏈接。 |
none() | 只保留文本,其他所有的html內容均被刪除 |
preserveRelativeLinks(booleanpreserve) | false(默認):不保留相對地址的url;true:保留相對地址的url |
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,如果需要可以手動添加。 |
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過濾類
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; /** * 配置過濾化參數(shù),不對代碼進行格式化 */ private Document.OutputSettings outputSettings; private HtmlFilter() { } /** * 靜態(tài)創(chuàng)建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; } /** * 靜態(tài)創(chuàng)建HtmlFilter方法 * @return HtmlFilter */ public static HtmlFilter create() { return create(null, false); } /** * 靜態(tài)創(chuàng)建HtmlFilter方法 * @param whiteList 白名單標簽 * @return HtmlFilter */ public static HtmlFilter create(Whitelist whiteList) { return create(whiteList, false); } /** * 靜態(tài)創(chuàng)建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; } /** * 靜態(tài)創(chuàng)建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來傳遞參數(shù)。
但是,如果在某些場景下面,通過requestBody體(json等),來傳遞相應參數(shù)應該怎么辦?
這就要需要我們對request的inputStream來進行來過濾處理了
有個地方需要注意一下的:
servlet中inputStream只能一次讀取,后續(xù)不能再次讀取inputStream。Xss過濾器中讀取了stream之后,后續(xù)如果其他邏輯涉及到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只能一次讀取,后續(xù)不能再次讀取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
到此這篇關于springboot2.x使用Jsoup防XSS攻擊的實現(xiàn)的文章就介紹到這了,更多相關springboot2.x防XSS攻擊內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java?spring注解@PostConstruct實戰(zhàn)案例講解
我們在Spring項目中經常會遇到@PostConstruct注解,可能有的伙伴對這個注解很陌生,下面這篇文章主要給大家介紹了關于Java?spring注解@PostConstruct實戰(zhàn)案例講解的相關資料,需要的朋友可以參考下2023-12-12