漏洞警告:SpringBoot 該如何預(yù)防 XSS 攻擊 ??
大家好,我是寶哥!
前言
寫此文章的目的是為了記錄一下在工作中解決的 XSS漏洞 問題。XSS漏洞是生產(chǎn)上比較常見的問題。雖然是比較常見并且是基本的安全問題,但是我們沒有做??? ,也怪我沒有安全意識。于是終于有一天被制裁了。所以這次就補上了,記錄一下。
正文
看看問題
XSS 漏洞到底是什么,說實話我講不太清楚。但是可以通過遇到的現(xiàn)象了解一下。在前端Form表單的輸入框中,用戶沒有正常輸入,而是輸入了一段代碼:
</input><img src=1 onerror=alert1>
?這個正常保存沒有問題。問題出在了列表查詢的時候,上面的代碼就生效了,由于圖片的地址亂寫的,所以這個alert就起作用了來看圖。

那根據(jù)這個原理,實際上如果沒有做任何的限制,有心人就可以為所欲為了??梢栽诶锩媲度胍恍╆P(guān)鍵代碼,把你的信息拿走。確實是個很嚴重的問題。
解決思路
既然是因為輸入框中輸入了不該輸入的東西,那自然就萌生一些想法:
- 校驗輸入內(nèi)容,不允許用戶輸入特殊字符,特殊標簽
- 允許用戶輸入,但是保存的時候?qū)⑻厥獾淖址苯犹鎿Q為空串
- 允許用戶輸入,將特殊字符轉(zhuǎn)譯保存。
第一種方法,特殊字符過濾。既然要過濾特殊字符,那就得自己把所有的特殊字符列出來進行匹配,比較麻煩,而且要定義好什么才是特殊字符?況且用戶本身不知道什么是特殊字符。突如其來的報錯,會讓用戶有點摸不著頭腦,不是很友好。
第二種方法,特殊字符替換為空串。未免有點太暴力。萬一真的需要輸入一點特殊的字符,保存完查出來發(fā)現(xiàn)少了好多東西,人家以為我們的BUG呢。也不是很好的辦法。
第三種辦法,特殊字符轉(zhuǎn)譯。這個辦法不但用戶數(shù)據(jù)不丟失,而且瀏覽器也不會執(zhí)行代碼。比較符合預(yù)期。
那辦法確定了,怎么做呢?前端來做還是后端來做?想了想還是要后端來做。畢竟使用切面或者Filter可以一勞永逸。
心路歷程
經(jīng)過抄襲,我發(fā)現(xiàn)了一些問題,也漸漸的有了一些理解。下面再說幾句廢話:
查到的預(yù)防XSS攻擊的,大多數(shù)的流程是:
- 攔截請求
- 重新包裝請求
-
重寫
HttpServletRequest中的獲取參數(shù)的方法 - 將獲得的參數(shù)進行XSS處理
- 攔截器放行
于是我就逮住一個抄了一下。抄襲完畢例行測試,發(fā)現(xiàn)我用
@RequestBody
接受的參數(shù),并不能過濾掉特殊字符。怎么肥四?大家明明都這么寫。為什么我的不好使?
這個時候突然一個想法萌生。SpringMVC在處理
@RequestBody
類型的參數(shù)的時候,是不是使用的我重寫的這些方法呢?(
getQueryString()
、
getParameter(String name)
、
getParameterValues(String name)
、
getParameterMap()
)。打了個日志,發(fā)現(xiàn)還真不是這些方法。
于是搜索了一下Springboot攔截器獲取
@RequestBody
參數(shù),碰到了這篇文章。首先的新發(fā)現(xiàn)是Spring MVC 在獲取
@RequestBody
參數(shù)的時候使用的是
getInputStream()
方法。嗯?(斜眼笑)那我是不是可以重寫這個方法獲取到輸入流的字符串,然后直接處理一下?
說干就干,一頓操作。進行測試。發(fā)現(xiàn)直接JSON 轉(zhuǎn)換的報錯了。腦裂。估計是獲得的字符串在轉(zhuǎn)換的時候把不該轉(zhuǎn)的東西轉(zhuǎn)譯了,導(dǎo)致不能序列化了。眼看就要成功了,一測回到解放前。
該怎么辦呢?其實思路是沒錯的,就是在獲取到流之后進行處理。但是錯就錯在處理的位置。果然處理的時間點很重要。(就像伴侶一樣,某人出現(xiàn)的時間點很重要)。那既然不能在現(xiàn)在處理,那就等他序列化完畢之后再處理就好了。那怎么辦呢?難道要寫一個AOP 攔截到所有的請求?用JAVA反射處理?
正在迷茫的時候,看到了一篇文章,知識增加了。原來可以在序列化和反序列化的時候進行處理。
最終實現(xiàn)
看一下最終的代碼實現(xiàn)(有些導(dǎo)入的包被我刪了)
重新包裝Request的代碼
import?org.apache.commons.text.StringEscapeUtils;
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
import?javax.servlet.ReadListener;
import?javax.servlet.ServletInputStream;
import?javax.servlet.http.HttpServletRequest;
import?javax.servlet.http.HttpServletRequestWrapper;
import?java.io.BufferedReader;
import?java.io.ByteArrayInputStream;
import?java.io.IOException;
import?java.io.InputStreamReader;
import?java.nio.charset.StandardCharsets;
import?java.util.Map;
/**
?*?重新包裝一下Request。重寫一些獲取參數(shù)的方法,將每個參數(shù)都進行過濾
?*/
public?class?XSSHttpServletRequestWrapper?extends?HttpServletRequestWrapper?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(XSSHttpServletRequestWrapper.class);
????private?HttpServletRequest?request;
????/**
?????*?請求體?RequestBody
?????*/
????private?String?reqBody;
????/**
?????*?Constructs?a?request?object?wrapping?the?given?request.
?????*
?????*?@param?request?The?request?to?wrap
?????*?@throws?IllegalArgumentException?if?the?request?is?null
?????*/
????public?XSSHttpServletRequestWrapper(HttpServletRequest?request)?{
????????super(request);
????????logger.info("---xss?XSSHttpServletRequestWrapper?created-----");
????????this.request?=?request;
????????reqBody?=?getBodyString();
????}
????@Override
????public?String?getQueryString()?{
????????return?StringEscapeUtils.escapeHtml4(super.getQueryString());
????}
????/**
?????*?The?default?behavior?of?this?method?is?to?return?getParameter(String
?????*?name)?on?the?wrapped?request?object.
?????*
?????*?@param?name
?????*/
????@Override
????public?String?getParameter(String?name)?{
????????logger.info("---xss?XSSHttpServletRequestWrapper?work??getParameter-----");
????????String?parameter?=?request.getParameter(name);
????????if?(StringUtil.isNotBlank(parameter))?{
????????????logger.info("----filter?before--name:{}--value:{}----",?name,?parameter);
????????????parameter?=?StringEscapeUtils.escapeHtml4(parameter);
????????????logger.info("----filter?after--name:{}--value:{}----",?name,?parameter);
????????}
????????return?parameter;
????}
????/**
?????*?The?default?behavior?of?this?method?is?to?return
?????*?getParameterValues(String?name)?on?the?wrapped?request?object.
?????*
?????*?@param?name
?????*/
????@Override
????public?String[]?getParameterValues(String?name)?{
????????logger.info("---xss?XSSHttpServletRequestWrapper?work??getParameterValues-----");
????????String[]?parameterValues?=?request.getParameterValues(name);
????????if?(!CollectionUtil.isEmpty(parameterValues))?{
?????????//?經(jīng)?“@Belief_7”?指正?這種方式不能更改parameterValues里面的值,要換成下面??的寫法
????????????//for?(String?value?:?parameterValues)?{
????????????//????logger.info("----filter?before--name:{}--value:{}----",?name,?value);
????????????//????value?=?StringEscapeUtils.escapeHtml4(value);
????????????//????logger.info("----filter?after--name:{}--value:{}----",?name,?value);
????????????//?}
????????????for?(int?i?=?0;?i?<?parameterValues.length;?i++)?
?????????{?
?????????????parameterValues[i]?=?StringEscapeUtils.escapeHtml4(parameterValues[i]);?
?????????}?
????????}
????????return?parameterValues;
????}
????/**
?????*?The?default?behavior?of?this?method?is?to?return?getParameterMap()?on?the
?????*?wrapped?request?object.
?????*/
????@Override
????public?Map<String,?String[]>?getParameterMap()?{
????????logger.info("---xss?XSSHttpServletRequestWrapper?work??getParameterMap-----");
????????Map<String,?String[]>?map?=?request.getParameterMap();
????????if?(map?!=?null?&&?!map.isEmpty())?{
????????????for?(String[]?value?:?map.values())?{
????????????????/*循環(huán)所有的value*/
????????????????for?(String?str?:?value)?{
????????????????????logger.info("----filter?before--value:{}----",?str,?str);
????????????????????str?=?StringEscapeUtils.escapeHtml4(str);
????????????????????logger.info("----filter?after--value:{}----",?str,?str);
????????????????}
????????????}
????????}
????????return?map;
????}
????/*重寫輸入流的方法,因為使用RequestBody的情況下是不會走上面的方法的*/
????/**
?????*?The?default?behavior?of?this?method?is?to?return?getReader()?on?the
?????*?wrapped?request?object.
?????*/
????@Override
????public?BufferedReader?getReader()?throws?IOException?{
????????logger.info("---xss?XSSHttpServletRequestWrapper?work??getReader-----");
????????return?new?BufferedReader(new?InputStreamReader(getInputStream()));
????}
????/**
?????*?The?default?behavior?of?this?method?is?to?return?getInputStream()?on?the
?????*?wrapped?request?object.
?????*/
????@Override
????public?ServletInputStream?getInputStream()?throws?IOException?{
????????logger.info("---xss?XSSHttpServletRequestWrapper?work??getInputStream-----");
????????/*創(chuàng)建字節(jié)數(shù)組輸入流*/
????????final?ByteArrayInputStream?bais?=?new?ByteArrayInputStream(reqBody.getBytes(StandardCharsets.UTF_8));
????????return?new?ServletInputStream()?{
????????????@Override
????????????public?boolean?isFinished()?{
????????????????return?false;
????????????}
????????????@Override
????????????public?boolean?isReady()?{
????????????????return?false;
????????????}
????????????@Override
????????????public?void?setReadListener(ReadListener?listener)?{
????????????}
????????????@Override
????????????public?int?read()?throws?IOException?{
????????????????return?bais.read();
????????????}
????????};
????}
????/**
?????*?獲取請求體
?????*
?????*?@return?請求體
?????*/
????private?String?getBodyString()?{
????????StringBuilder?builder?=?new?StringBuilder();
????????InputStream?inputStream?=?null;
????????BufferedReader?reader?=?null;
????????try?{
????????????inputStream?=?request.getInputStream();
????????????reader?=?new?BufferedReader(new?InputStreamReader(inputStream));
????????????String?line;
????????????while?((line?=?reader.readLine())?!=?null)?{
????????????????builder.append(line);
????????????}
????????}?catch?(IOException?e)?{
????????????logger.error("-----get?Body?String?Error:{}----",?e.getMessage(),?e);
????????}?finally?{
????????????if?(inputStream?!=?null)?{
????????????????try?{
????????????????????inputStream.close();
????????????????}?catch?(IOException?e)?{
????????????????????logger.error("-----get?Body?String?Error:{}----",?e.getMessage(),?e);
????????????????}
????????????}
????????????if?(reader?!=?null)?{
????????????????try?{
????????????????????reader.close();
????????????????}?catch?(IOException?e)?{
????????????????????logger.error("-----get?Body?String?Error:{}----",?e.getMessage(),?e);
????????????????}
????????????}
????????}
????????return?builder.toString();
????}
}
定義過濾器
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
import?javax.servlet.*;
import?javax.servlet.http.HttpServletRequest;
import?java.io.IOException;
/**
?*?Filter?過濾器,攔截請求轉(zhuǎn)換為新的請求
?*/
public?class?XssFilter?implements?Filter?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(XssFilter.class);
????/**
?????*?初始化方法
?????*/
????@Override
????public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
????????logger.info("----xss?filter?start-----");
????}
????/**
?????*?過濾方法
?????*/
????@Override
????public?void?doFilter(ServletRequest?request,?ServletResponse?response,?FilterChain?chain)?throws?IOException,?ServletException?{
????????ServletRequest?wrapper?=?null;
????????if?(request?instanceof?HttpServletRequest)?{
????????????HttpServletRequest?servletRequest?=?(HttpServletRequest)?request;
????????????wrapper?=?new?XSSHttpServletRequestWrapper(servletRequest);
????????}
????????if?(null?==?wrapper)?{
????????????chain.doFilter(request,?response);
????????}?else?{
????????????chain.doFilter(wrapper,?response);
????????}
????}
}
注冊過濾器
注冊過濾器我了解到的有兩種方式。我用的下面的這種
一種通過
@WebFilter
注解的方式來配置,但這種啟動類上要加
@ServletComponentScan
?注解來指定掃描路徑
另外一種就是以Bean 的方式來注入(不知道放哪里,就把Bean放到啟動類里面)
/**
?*?XSS?的Filter注入
?*?用來處理getParameter的參數(shù)
?*?@return
?*/
@Bean
public?FilterRegistrationBean?xssFilterRegistrationBean(){
????FilterRegistrationBean?filterRegistrationBean?=?new?FilterRegistrationBean();
????filterRegistrationBean.setFilter(new?XssFilter());
????filterRegistrationBean.setOrder(1);
????filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);
????filterRegistrationBean.setEnabled(true);
????filterRegistrationBean.addUrlPatterns("/*");
????return?filterRegistrationBean;
}
上面配的是使用
request.getParameter()
的時候生效的,但是當我使用
@RequestBody
來接收參數(shù)的時候是不行的,所以還得有下面的代碼:
處理請求中的JSON數(shù)據(jù)
import?com.fasterxml.jackson.core.JsonParser;
import?com.fasterxml.jackson.core.JsonProcessingException;
import?com.fasterxml.jackson.databind.DeserializationContext;
import?com.fasterxml.jackson.databind.JsonDeserializer;
import?org.apache.commons.text.StringEscapeUtils;
import?java.io.IOException;
/**
?*?反序列化,用來處理請求中的JSON數(shù)據(jù)
?*?處理RequestBody方式接收的參數(shù)
?*/
public?class?XssJacksonDeserializer?extends?JsonDeserializer<String>?{
????@Override
????public?String?deserialize(JsonParser?jp,?DeserializationContext?ctxt)?throws?IOException,?JsonProcessingException?{
????????return?StringEscapeUtils.escapeHtml4(jp.getText());
????}
}
處理返回值的JSON數(shù)據(jù)
import?com.fasterxml.jackson.core.JsonGenerator;
import?com.fasterxml.jackson.databind.JsonSerializer;
import?com.fasterxml.jackson.databind.SerializerProvider;
import?org.apache.commons.text.StringEscapeUtils;
import?java.io.IOException;
/**
?*?處理向前端發(fā)送的JSON數(shù)據(jù),將數(shù)據(jù)進行轉(zhuǎn)譯后發(fā)送
?*/
public?class?XssJacksonSerializer?extends?JsonSerializer<String>?{
????@Override
????public?void?serialize(String?value,?JsonGenerator?jgen,?SerializerProvider?provider)?throws?IOException?{
????????jgen.writeString(StringEscapeUtils.escapeHtml4(value));
????}
}
注冊、配置自定義的序列化方法
@Override
public?void?extendMessageConverters(List<HttpMessageConverter<?>>?converters)?{
?Jackson2ObjectMapperBuilder?builder?=?new?Jackson2ObjectMapperBuilder();
?ObjectMapper?mapper?=?builder.build();
?/*注入自定義的序列化工具,將RequestBody的參數(shù)進行轉(zhuǎn)譯后傳輸*/
????SimpleModule?simpleModule?=?new?SimpleModule();
????//?XSS序列化
????simpleModule.addSerializer(String.class,?new?XssJacksonSerializer());
????simpleModule.addDeserializer(String.class,?new?XssJacksonDeserializer());
????mapper.registerModule(simpleModule);
????converters.add(new?MappingJackson2HttpMessageConverter(mapper));
}
測試
所有東西都配置完了,接下來進行愉快的測試階段了。
我依然在輸入框中輸入這段代碼
</input><img src=1 onerror=alert1>
并進行保存。來看一下數(shù)據(jù)庫中的保存結(jié)果:

可以看到數(shù)據(jù)庫中保存的數(shù)據(jù),已經(jīng)經(jīng)過轉(zhuǎn)譯了。那查詢一下列表是什么樣的呢?

可以看到兩條數(shù)據(jù),上面的是我們經(jīng)過轉(zhuǎn)譯的,正常的展示出來了。而下面的是沒經(jīng)過轉(zhuǎn)譯的,直接空白,并且給我彈了個窗。
總結(jié)
- 就是注意要分情況處理。
- 攔截器處理一部分,并注意攔截器的注冊方式
- Jackson的方式處理另一部分,也是注意配置方式
補充
代碼經(jīng)過驗證后,發(fā)現(xiàn)了一個問題。今天來補充一下。問題是這樣的:
如果使用
@RequestBody
的形式接受參數(shù),也就是需要使用自定義的序列化方式。然而有時候,我們的業(yè)務(wù)需要傳遞一些JSON串到后端,如
{\"username\":\"zx\",\"pwd\":\"123\"}
(注意這是個字符串)。但是因為我不管三七二十一直接暴力轉(zhuǎn)譯,導(dǎo)致里面的雙引號以及其他符號都被轉(zhuǎn)譯了。那么當我們拿到這個字符串之后,再自己反序列化的時候就會出錯了。
為了解決這個問題,我在自定義的序列化方法中判斷了一下這個字段的值是否是JSON形式,如果是JSON形式,那就不做處理,直接返回,以保證能夠順利反序列化。判斷是否是JSON的方式,我選擇最簡單的,判斷首尾是否是
{ } [ ]
的組合。代碼如下:
public?class?XssJacksonDeserializer?extends?JsonDeserializer<String>?{
????@Override
????public?String?deserialize(JsonParser?jp,?DeserializationContext?ctxt)?throws?IOException,?JsonProcessingException?{
????????//?判斷一下?值是不是JSON的格式,如果是JSON的話,那就不處理了。
????????/*判斷JSON,可以用JSON.parse但是所有字段都Parse一下,未免有點太費性能,所以粗淺的認為,不是以{?或者[?開頭的文本都不是JSON*/
????????if?(isJson(jp.getText()))?{
????????????return?jp.getText();
????????}
????????return?StringEscapeUtils.escapeHtml4(jp.getText());
????}
????/**
?????*?判斷字符串是不是JSON
?????*
?????*?@param?str
?????*?@return
?????*/
????private?boolean?isJson(String?str)?{
????????boolean?result?=?false;
????????if?(StringUtil.isNotBlank(str))?{
????????????str?=?str.trim();
????????????if?(str.startsWith("{")?&&?str.endsWith("}"))?{
????????????????result?=?true;
????????????}?else?if?(str.startsWith("[")?&&?str.endsWith("]"))?{
????????????????result?=?true;
????????????}
????????}
????????return?result;
????}
}
但是經(jīng)過這樣的改動之后,可能又沒那么安全了。所以還是要看自己的取舍了。
對此,你有什么想補充的?歡迎在評論區(qū)留言~
來源:blog.csdn.net/sinat_31420295/ article/ details/121519010
精彩推薦:
SpringBoot接入輕量級分布式日志框架GrayLog實戰(zhàn)!
現(xiàn)在的公司都這么卷了?自家這么牛逼的統(tǒng)一管理平臺開源了!
JWT 實現(xiàn)登錄認證 + Token 自動續(xù)期方案,這才是正確的使用姿勢!
別用XShell了,這款SSH工具絕對驚艷,還支持網(wǎng)頁版.....
大文件上傳下載實現(xiàn)思路,分片、斷點續(xù)傳代碼實現(xiàn)
