SpringBoot預防XSS攻擊的實現
XSS 漏洞到底是什么,說實話我講不太清楚。但是可以通過遇到的現象了解一下。在前端Form表單的輸入框中,用戶沒有正常輸入,而是輸入了一段代碼: </input><img src=1 onerror=alert1>
這個正常保存沒有問題。問題出在了列表查詢的時候,上面的代碼就生效了,由于圖片的地址亂寫的,所以這個alert就起作用了來看圖。
那根據這個原理,實際上如果沒有做任何的限制,有心人就可以為所欲為了??梢栽诶锩媲度胍恍╆P鍵代碼,把你的信息拿走。確實是個很嚴重的問題。
解決思路
既然是因為輸入框中輸入了不該輸入的東西,那自然就萌生一些想法:
- 校驗輸入內容,不允許用戶輸入特殊字符,特殊標簽
- 允許用戶輸入,但是保存的時候將特殊的字符直接替換為空串
- 允許用戶輸入,將特殊字符轉譯保存。
第一種方法,特殊字符過濾。既然要過濾特殊字符,那就得自己把所有的特殊字符列出來進行匹配,比較麻煩,而且要定義好什么才是特殊字符?況且用戶本身不知道什么是特殊字符。突如其來的報錯,會讓用戶有點摸不著頭腦,不是很友好。
第二種方法,特殊字符替換為空串。未免有點太暴力。萬一真的需要輸入一點特殊的字符,保存完查出來發(fā)現少了好多東西,人家以為我們的BUG呢。也不是很好的辦法。
第三種辦法,特殊字符轉譯。這個辦法不但用戶數據不丟失,而且瀏覽器也不會執(zhí)行代碼。比較符合預期。
那辦法確定了,怎么做呢?前端來做還是后端來做?想了想還是要后端來做。畢竟使用切面或者Filter可以一勞永逸。
心路歷程
經過抄襲,我發(fā)現了一些問題,也漸漸的有了一些理解。下面再說幾句廢話:
查到的預防XSS攻擊的,大多數的流程是:
- 攔截請求
- 重新包裝請求
- 重寫
HttpServletRequest
中的獲取參數的方法 - 將獲得的參數進行XSS處理
- 攔截器放行
于是我就逮住一個抄了一下。抄襲完畢例行測試,發(fā)現我用 @RequestBody
接受的參數,并不能過濾掉特殊字符。怎么肥四?大家明明都這么寫。為什么我的不好使?
這個時候突然一個想法萌生。SpringMVC在處理 @RequestBody
類型的參數的時候,是不是使用的我重寫的這些方法呢?( getQueryString()
、 getParameter(String name)
、 getParameterValues(String name)
、 getParameterMap()
)。打了個日志,發(fā)現還真不是這些方法。
于是搜索了一下Springboot攔截器獲取 @RequestBody
參數,碰到了這篇文章。首先的新發(fā)現是Spring MVC 在獲取 @RequestBody
參數的時候使用的是 getInputStream()
方法。嗯?(斜眼笑)那我是不是可以重寫這個方法獲取到輸入流的字符串,然后直接處理一下?
說干就干,一頓操作。進行測試。發(fā)現直接JSON 轉換的報錯了。腦裂。估計是獲得的字符串在轉換的時候把不該轉的東西轉譯了,導致不能序列化了。眼看就要成功了,一測回到解放前。
該怎么辦呢?其實思路是沒錯的,就是在獲取到流之后進行處理。但是錯就錯在處理的位置。果然處理的時間點很重要。(就像伴侶一樣,某人出現的時間點很重要)。那既然不能在現在處理,那就等他序列化完畢之后再處理就好了。那怎么辦呢?難道要寫一個AOP 攔截到所有的請求?用JAVA反射處理?
正在迷茫的時候,看到了一篇文章,知識增加了。原來可以在序列化和反序列化的時候進行處理。
最終實現
看一下最終的代碼實現(有些導入的包被我刪了)
重新包裝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。重寫一些獲取參數的方法,將每個參數都進行過濾 */ 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)) { // 經 “@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é)數組輸入流*/ 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 過濾器,攔截請求轉換為新的請求 */ 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的參數 * @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
來接收參數的時候是不行的,所以還得有下面的代碼:
處理請求中的JSON數據
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數據 * 處理RequestBody方式接收的參數 */ public class XssJacksonDeserializer extends JsonDeserializer<String> { @Override public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { return StringEscapeUtils.escapeHtml4(jp.getText()); } }
處理返回值的JSON數據
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數據,將數據進行轉譯后發(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的參數進行轉譯后傳輸*/ 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>
并進行保存。來看一下數據庫中的保存結果:
圖片
可以看到數據庫中保存的數據,已經經過轉譯了。那查詢一下列表是什么樣的呢?
可以看到兩條數據,上面的是我們經過轉譯的,正常的展示出來了。而下面的是沒經過轉譯的,直接空白,并且給我彈了個窗。
總結
就是注意要分情況處理。
攔截器處理一部分,并注意攔截器的注冊方式
Jackson的方式處理另一部分,也是注意配置方式
補充
代碼經過驗證后,發(fā)現了一個問題。今天來補充一下。問題是這樣的:
如果使用 @RequestBody
的形式接受參數,也就是需要使用自定義的序列化方式。然而有時候,我們的業(yè)務需要傳遞一些JSON串到后端,如 {\"username\":\"zx\",\"pwd\":\"123\"}
(注意這是個字符串)。但是因為我不管三七二十一直接暴力轉譯,導致里面的雙引號以及其他符號都被轉譯了。那么當我們拿到這個字符串之后,再自己反序列化的時候就會出錯了。
為了解決這個問題,我在自定義的序列化方法中判斷了一下這個字段的值是否是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; } }
但是經過這樣的改動之后,可能又沒那么安全了。所以還是要看自己的取舍了。
到此這篇關于SpringBoot預防XSS攻擊的實現的文章就介紹到這了,更多相關SpringBoot預防XSS攻擊內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springboot之@ConfigurationProperties注解解讀
在Spring Boot中,@EnableConfigurationProperties注解的主要作用是激活@ConfigurationProperties注解的配置屬性類,從而讓配置屬性類能被Spring容器管理,這樣的話,我們就可以在屬性類中輕松地使用@ConfigurationProperties來綁定配置文件中的屬性2024-10-10Spring Boot整合Spring Security的示例代碼
這篇文章主要介紹了Spring Boot整合Spring Security的示例代碼,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04JVM入門之類加載與字節(jié)碼技術(類加載與類的加載器)
Java字節(jié)碼增強指的是在Java字節(jié)碼生成之后,對其進行修改,增強其功能,這種方式相當于對應用程序的二進制文件進行修改。Java字節(jié)碼增強主要是為了減少冗余代碼,提高性能等2021-06-06解決創(chuàng)建springboot后啟動報錯:Failed?to?bind?properties?under‘spri
在Spring?Boot項目中,application.properties和application.yml是用于配置參數的兩種文件格式,properties格式簡潔但不支持層次結構,而yml格式支持層次性,可讀性更好,在yml文件中,要注意細節(jié),比如冒號后面需要空格2024-10-10