跨站腳本攻擊(Cross Site Scripting),為了不和層疊樣式表(Cascading Style Sheets, CSS)的縮寫混淆,故將跨站腳本攻擊縮寫為XSS。惡意攻擊者往Web頁(yè)面里插入惡意Script代碼,當(dāng)用戶瀏覽該頁(yè)之時(shí),嵌入其中Web里面的Script代碼會(huì)被執(zhí)行,從而達(dá)到惡意攻擊用戶的目的。
你可以自己做個(gè)簡(jiǎn)單嘗試:
- 在任何一個(gè)表單內(nèi),你輸入一段簡(jiǎn)單的js代碼:
<script>for(var i=0;i<1000;i++){alert("彈死你"+i);}</script>,將其存入數(shù)據(jù)庫(kù); - 在頁(yè)面上一個(gè)div元素內(nèi)直接展示第一步內(nèi)存入的值,你會(huì)發(fā)現(xiàn)彈出框出現(xiàn)了;
以上XSS攻擊只算一個(gè)小惡作劇,但如果這玩意被發(fā)到了網(wǎng)站的首頁(yè)上,我估計(jì)老板一定會(huì)因?yàn)轭l繁的投訴而和你來場(chǎng)愉快的談話...
以上兩個(gè)示例僅僅算是惡作劇,惡意用戶能做的更多,如獲取用戶信息,進(jìn)行“網(wǎng)絡(luò)釣魚”攻擊等。
應(yīng)對(duì)XSS攻擊的其中一個(gè)方式就是后端對(duì)輸入內(nèi)容進(jìn)行過濾,輸入內(nèi)容里面的敏感信息直接過濾,如<script>標(biāo)簽等,以下來說明如何在spring boot項(xiàng)目?jī)?nèi)方便快捷的實(shí)現(xiàn)XSS過濾。
1、Jsoup組件
Jsoup使用標(biāo)簽白名單的機(jī)制用來進(jìn)行防止XSS攻擊, 假設(shè)白名單中只允許p標(biāo)簽存在, 此時(shí)在一段HTML代碼中, 只能存在p標(biāo)簽 , 其他標(biāo)簽將會(huì)被清除只保留被標(biāo)簽所包裹的內(nèi)容,因此使用Jsoup組件來進(jìn)行內(nèi)容過濾。
添加maven依賴:
<!-- xss過濾組件 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.9.2</version>
</dependency>
JsoupUtil提供基于Jsoup過濾非法標(biāo)簽的工具類:
/**
* xss非法標(biāo)簽過濾
* {@link http://www.itdecent.cn/p/32abc12a175a?nomobile=yes}
*/
public class JsoupUtil {
/**
* 使用自帶的basicWithImages 白名單
* 允許的便簽有a,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol,p,pre,q,small,span,
* strike,strong,sub,sup,u,ul,img
* 以及a標(biāo)簽的href,img標(biāo)簽的src,align,alt,height,width,title屬性
*/
private static final Whitelist whitelist = Whitelist.basicWithImages();
/** 配置過濾化參數(shù),不對(duì)代碼進(jìn)行格式化 */
private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
static {
// 富文本編輯時(shí)一些樣式是使用style來進(jìn)行實(shí)現(xiàn)的
// 比如紅色字體 style="color:red;"
// 所以需要給所有標(biāo)簽添加style屬性
whitelist.addAttributes(":all", "style");
}
public static String clean(String content) {
return Jsoup.clean(content, "", whitelist, outputSettings);
}
public static void main(String[] args) throws FileNotFoundException, IOException {
String text = "<a href=\"http://www.baidu.com/a\" onclick=\"alert(1);\">sss</a><script>alert(0);</script>sss";
System.out.println(clean(text));
}
}
2、創(chuàng)建XssHttpServletRequestWrapper
這是實(shí)現(xiàn)XSS過濾的關(guān)鍵,在其內(nèi)重寫了getParameter,getParameterValues,getHeader等方法,對(duì)http請(qǐng)求內(nèi)的參數(shù)進(jìn)行了過濾。
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方法,將參數(shù)名和參數(shù)值都做xss過濾。<br/>
* 如果需要獲得原始的值,則通過super.getParameterValues(name)來獲取<br/>
* 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方法,將參數(shù)名和參數(shù)值都做xss過濾。<br/>
* 如果需要獲得原始的值,則通過super.getHeaders(name)來獲取<br/>
* 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
*
* @return
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 獲取最原始的request的靜態(tài)方法
*
* @return
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
if (req instanceof XssHttpServletRequestWrapper) {
return ((XssHttpServletRequestWrapper) req).getOrgRequest();
}
return req;
}
}
3、創(chuàng)建XssFilter
XssFilter是過濾XSS請(qǐng)求的入口,在這里通過XssHttpServletRequestWrapper將HttpServletRequest進(jìn)行了封裝,filterChain.doFilter(xssRequest, response);保證了后續(xù)代碼執(zhí)行request.getParameter,request.getParameterValues,request.getHeader時(shí)調(diào)用的都是XssHttpServletRequestWrapper內(nèi)重寫的方法,獲取到的參數(shù)是已經(jīng)進(jìn)行過標(biāo)簽過濾的內(nèi)容,從而消除了敏感信息。
/**
* 攔截防止xss注入
* 通過Jsoup過濾請(qǐng)求參數(shù)內(nèi)的特定字符
* @author yangwk
*/
public class XssFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(XssFilter.class);
private static boolean IS_INCLUDE_RICH_TEXT = false;//是否過濾富文本內(nèi)容
public List<String> excludes = new ArrayList<String>();
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,ServletException {
if(logger.isDebugEnabled()){
logger.debug("xss filter is open");
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if(handleExcludeURL(req, resp)){
filterChain.doFilter(request, response);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request,IS_INCLUDE_RICH_TEXT);
filterChain.doFilter(xssRequest, response);
}
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;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if(logger.isDebugEnabled()){
logger.debug("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 destroy() {}
}
4、注冊(cè)XssFilter
通過java config的方式注冊(cè)XSSFilter,使其生效。
/**
* xss過濾攔截器
*/
@Bean
public FilterRegistrationBean xssFilterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new XssFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
Map<String, String> initParameters = Maps.newHashMap();
initParameters.put("excludes", "/favicon.ico,/img/*,/js/*,/css/*");
initParameters.put("isIncludeRichText", "true");
filterRegistrationBean.setInitParameters(initParameters);
return filterRegistrationBean;
}
- excludes用于配置不需要參數(shù)過濾的請(qǐng)求url
- isIncludeRichText默認(rèn)為true,主要用于設(shè)置富文本(項(xiàng)目?jī)?nèi)約束以content為名或以WithHtml結(jié)尾)內(nèi)容是否需要過濾,該選項(xiàng)可根據(jù)公司具體情況調(diào)整,建議約束富文本編輯框支持的標(biāo)簽并開啟改約束,減少安全隱患
小結(jié)
防御XSS攻擊,可以通過后端統(tǒng)一進(jìn)行標(biāo)簽過濾,去掉所有輸入內(nèi)容中包含的類似于<script>這樣的非法標(biāo)簽來實(shí)現(xiàn)。
- 標(biāo)簽過濾實(shí)現(xiàn)可使用Jsoup,功能強(qiáng)大,使用方便,更多內(nèi)容可參考Jsoup 防止富文本 XSS 攻擊;
- 繼承HttpServletRequestWrapper,重寫從request內(nèi)獲取參數(shù)的方法,在其內(nèi)調(diào)用JsoupUtil的方法,進(jìn)行參數(shù)脫敏處理;
- 通過XssFilter將XssHttpServletRequestWrapper設(shè)置入處理鏈中,從而達(dá)到后續(xù)處理類內(nèi)通過Request獲取參數(shù)時(shí)調(diào)用的是重寫后的獲取參數(shù)的方法,進(jìn)而達(dá)成業(yè)務(wù)代碼無(wú)感知的實(shí)現(xiàn)了XSS過濾的目的。
本人搭建好的spring boot web后端開發(fā)框架已上傳至GitHub,包含本文內(nèi)的全部代碼示例。
https://github.com/q7322068/rest-base,已用于多個(gè)正式項(xiàng)目,當(dāng)前可能因?yàn)榘姹締栴}不是很完善,后續(xù)持續(xù)優(yōu)化,希望你能有所收獲!