一杠园、問題背景
跨站腳本攻擊的英文全稱是Cross Site Script,為了和樣式表區(qū)分,縮寫為XSS陪竿。發(fā)生的原因是網(wǎng)站將用戶輸入的內(nèi)容輸出到頁面上戏挡,在這個過程中可能有惡意代碼被瀏覽器執(zhí)行听皿⊥┐牛跨站腳本攻擊,它指的是惡意攻擊者往Web頁面里插入惡意html代碼苹熏,當用戶瀏覽該頁之時呻畸,嵌入其中Web里面的html代碼會被執(zhí)行移盆,從而達到惡意用戶的特殊目的。已知的跨站腳本攻擊漏洞有三種:1)存儲式伤为;2)反射式咒循;3)基于DOM。
1绞愚、存儲型跨站腳本攻擊涉及的功能點:用戶輸入的文本信息保存到數(shù)據(jù)庫中叙甸,并能夠在頁面展示的功能點,例如用戶留言位衩、發(fā)送站內(nèi)消息裆蒸、個人信息修改等功能點。
2糖驴、反射型跨站腳本攻擊涉及的功能點:URL參數(shù)需要在頁面顯示的功能點都可能存在反射型跨站腳本攻擊僚祷,例如站內(nèi)搜索佛致、查詢功能點。
3辙谜、基于DOM跨站腳本攻擊涉及的功能點:涉及DOM對象的頁面程序俺榆,包括(不限這些)。
漏洞危害
常見的反射型跨站腳本攻擊步驟如下:
1)攻擊者創(chuàng)建并測試惡意URL装哆;
2)攻擊者確信受害者在瀏覽器中加載了惡意URL罐脊;
3)攻擊者采用反射型跨站腳本攻擊方式安裝鍵盤記錄器、竊取受害者的cookie 蜕琴、竊取剪貼板內(nèi)容萍桌、改變網(wǎng)頁內(nèi)容(例如下載鏈接)。
存儲型跨站腳本攻擊最為常見的場景是將跨站腳本寫入文本輸入域中凌简,如留言板上炎、博客或新聞發(fā)布系統(tǒng)的評論框。當用戶瀏覽留言和評論時号醉,瀏覽器執(zhí)行跨站腳本代碼反症。
例如,在某個系統(tǒng)的文本輸入框中輸入如下的示例攻擊代碼片段<script>alert(2)</script>
畔派,如果彈出如下的彈窗铅碍,則說明對應系統(tǒng)存在被XSS攻擊的風險:
加固建議:
總體修復方式:驗證所有輸入數(shù)據(jù),有效檢測攻擊线椰;對所有輸出數(shù)據(jù)進行適當?shù)木幋a胞谈,以防止任何已成功注入的腳本在瀏覽器端運行。具體如下 :
1)輸入驗證:某個數(shù)據(jù)被接受為可被顯示或存儲之前憨愉,使用標準輸入驗證機制烦绳,驗證所有輸入數(shù)據(jù)的長度、類型配紫、語法以及業(yè)務規(guī)則径密。
2)輸出編碼:數(shù)據(jù)輸出前,確保用戶提交的數(shù)據(jù)已被正確進行entity編碼躺孝,建議對所有字符進行編碼而不僅局限于某個子集享扔。
3)明確指定輸出的編碼方式:不要允許攻擊者為你的用戶選擇編碼方式(如ISO 8859-1或 UTF 8)。
4)注意黑名單驗證方式的局限性:僅僅查找或替換一些字符(如"<" ">"或類似"script"的關鍵字)植袍,很容易被XSS變種攻擊繞過驗證機制惧眠。
5)警惕規(guī)范化錯誤:驗證輸入之前,必須進行解碼及規(guī)范化以符合應用程序當前的內(nèi)部表示方法于个。請確定應用程序?qū)ν惠斎氩蛔鰞纱谓獯a氛魁。對客戶端提交的數(shù)據(jù)進行過濾,一般建議過濾掉雙引號(”)、尖括號(<秀存、>)等特殊字符捶码,或者對客戶端提交的數(shù)據(jù)中包含的特殊字符進行實體轉(zhuǎn)換,比如將雙引號(”)轉(zhuǎn)換成其實體形式"或链,<對應的實體形式是<宙项,<對應的實體形式是>以下為需過濾的常見字符:
6)對參數(shù)中的特殊字符進行轉(zhuǎn)義或者編碼,如:“’株扛、”、<汇荐、>洞就、(、=掀淘、.”等特殊字符旬蟋。
二、修復方案
2.1 統(tǒng)一對輸入?yún)?shù)進行攔截和過濾
編寫一個Filter過濾器XSSFilter.java
public class XSSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
/*
* //設置request字符編碼 request.setCharacterEncoding("UTF-8");
* //設置response字符編碼 response.setContentType("text/wrapper;charset=UTF-8");
*/
HttpServletRequest req = (HttpServletRequest) servletRequest;
filterChain.doFilter(new XSSRequestWrapper(req), servletResponse);
}
@Override
public void destroy() {
}
}
接下來需要將該Filter注入到Spring框架中革娄,首先介紹傳統(tǒng)的xml配置的方式:
<!-- 配置過濾器 -->
<filter>
<filter-name>XSSFilter</filter-name>
<filter-class>com.test.filter.XSSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>XSSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
如果是SpringBoot應用倾贰,可以采用如下的配置類的方式:
@Component
public class FilterRegistration {
/**
* 配置過濾器
*
* @return
*/
@Bean
@Order(Integer.MAX_VALUE)
public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(XSSFilter());
registration.addUrlPatterns("/*");
//registration.setOrder(Integer.MAX_VALUE);//過濾器順序,也可通過@Order注解配置
//registration.addInitParameter("paramName", "paramValue");
registration.setName("XSSFilter");
return registration;
}
/**
* 創(chuàng)建一個bean
*
* @return
*/
@Bean(name = "XSSFilter")
public Filter XSSFilter() {
return new XSSFilter();
}
}
這里特別說明一下拦惋,在一個web應用中匆浙,可以開發(fā)編寫多個Filter,這些Filter組合起來稱為一個Filter鏈厕妖。那么有哪些方法可以配置多個Filter之間處理的優(yōu)先級呢首尼?
如果采用XML配置的方式,web服務器根據(jù)Filter在web.xml中的注冊順序言秸,決定先調(diào)用哪個Filter软能,當?shù)谝粋€Filter的doFilter方法被調(diào)用時,web服務器會創(chuàng)建一個代表Filter鏈的FilterChain對象傳遞給該方法举畸,在doFilter方法中查排,開發(fā)人員如果調(diào)用了FilterChain對象的doFilter方法,則web服務器會檢查FilterChain對象中是否還有filter抄沮,如果有跋核,則調(diào)用第二個filter,如果沒有合是,則調(diào)用目標資源了罪。詳情參考資料1。
如果是采用配置類的方式聪全,可以通過 @Order(Integer.MAX_VALUE)
注解泊藕,或者registration.setOrder(Integer.MAX_VALUE);
其中,填寫的參數(shù)值越小,執(zhí)行的優(yōu)先級就越高娃圆。
撰寫一個判斷JSON格式是否合法的工具類JSONUtils:
public class JSONUtils {
/**
* Jackson library
*
* @param jsonInString
* @return
*/
public final static boolean isJSONValid(String jsonInString) {
try {
final ObjectMapper mapper = new ObjectMapper();
mapper.readTree(jsonInString);
return true;
} catch (IOException e) {
return false;
}
}
public final static boolean isJSONValid1(String test) {
try {
JSONObject.parseObject(test);
} catch (JSONException ex) {
try {
JSONObject.parseArray(test);
} catch (JSONException ex1) {
return false;
}
}
return true;
}
}
接下來是重頭戲玫锋,由于默認的HttpServletRequest是不允許對請求參數(shù)和請求路徑進行直接修改的(參數(shù)是只讀的),如果需要修改就需要繼承HttpServletRequestWrapper
類包裝一下讼呢,在其提供的鉤子方法中修改參數(shù)的返回值撩鹿。
public class XSSRequestWrapper extends HttpServletRequestWrapper {
private String body;
private static String[] NO_CHECK_PARAMETER_NAME_LIST = new String[]{};
private static String[] NO_CHECK_URL_LIST = new String[]{"/test1"};
private static List<Pattern> NO_CHECK_URL_PATTERN_LIST = Arrays.stream(NO_CHECK_URL_LIST).map(item -> Pattern.compile(item)).collect(Collectors.toList());
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getQueryString() {
return SecurityUtil.escapeHtmlWithoutAmpersand(super.getQueryString());
}
@Override
public Object getAttribute(String name) {
return super.getAttribute(name);
}
/**
* 重寫父類方法
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return stripXSS(name, value);
}
/**
* 重寫父類方法
*/
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
return stripXSS(parameter, value);
}
/**
* 處理query String參數(shù)
*/
@Override
public String[] getParameterValues(String parameter) {
String path = super.getServletPath();
String[] values = super.getParameterValues(parameter);
// 排除沒有請求參數(shù)和無需校驗的路徑
if (values == null || enableNoCheckURL(NO_CHECK_URL_PATTERN_LIST, path)) {
return values;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(parameter, values[i]);
}
return encodedValues;
}
@Override
public ServletInputStream getInputStream() throws IOException {
body = HttpGetBody.getBodyString(super.getRequest());
ServletInputStream inputStream = null;
if (StringUtils.isNotEmpty(body)) {
Map<String, Object> paramMap = JSON.parseObject(body);
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
paramMap.put(StringEscapeUtils.escapeHtml4(entry.getKey()),
StringEscapeUtils.escapeHtml4(entry.getValue().toString()));
}
body = JSON.toJSONString(paramMap);
inputStream = new PostServletInputStream(body);
}
return inputStream;
}
@Override
public BufferedReader getReader() throws IOException {
body = HttpGetBody.getBodyString(super.getRequest());
String encoding = getCharacterEncoding();
if (encoding == null) {
encoding = "UTF-8";
}
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 過濾參數(shù)
*
* @param value 參數(shù)值
* @param parameter 參數(shù)name名
* @return
*/
private String stripXSS(String parameter, String value) {
String newValue = value;
if (newValue != null && enableNoCheckParameter(parameter)) {
if (JSONUtils.isJSONValid(newValue)) {
// HtmlUtils.htmlEscape(value);
// StringEscapeUtils.unescapeJson(value);
newValue = SecurityUtil.escapeJsonWithoutAmpersand(value);
} else {
// StringEscapeUtils.escapeHtml4(value);
// HtmlUtils.htmlEscape(value);//spring的HtmlUtils進行轉(zhuǎn)義
newValue = SecurityUtil.escapeHtmlWithoutAmpersand(value);
}
}
return newValue;
}
/**
* 判斷name是否應該攔截
*
* @param parameter 參數(shù)名
* @return 不攔截返回true,攔截返回false
*/
private boolean enableNoCheckParameter(String parameter) {
for (String parameters : NO_CHECK_PARAMETER_NAME_LIST) {
if (parameter.equals(parameters)) {
return false;
}
}
return true;
}
/**
* 判斷傳入的uri是否滿足patter
*
* @param exclusionPatterns
* @param uri
* @return
*/
public static boolean enableNoCheckURL(List<Pattern> exclusionPatterns, String uri) {
if (exclusionPatterns != null) {
uri = uri.trim();
for (Pattern exclusionPattern : exclusionPatterns) {
if (isWildCardMatched(uri, exclusionPattern)) {
return true;
}
}
}
return false;
}
/**
* 對指定的文本進行模糊匹配悦屏,支持* 和?节沦,不區(qū)分大小寫
*
* @param text 要進行模糊匹配的文本
* @param pattern 模糊匹配表達式
* @return
*/
public static boolean isWildCardMatched(String text, Pattern pattern) {
Matcher m = pattern.matcher(text);
return m.matches();
}
}
這里重點重寫了兩個方法:getParameter和getParameterValues,getParameter方法是直接通過request獲得querystring類型的入?yún)⒄{(diào)用的方法础爬。如果是通過springMVC注解類型來獲得參數(shù)的話甫贯,走的是getParameterValues的方法。同時提供了enableNoCheckURL
和enableNoCheckParameter
來實現(xiàn)部分參數(shù)和URL地址免過濾白名單的功能看蚜,這個白名單可以寫死叫搁,也可以通過微服務框架中常用的配置管理中心動態(tài)獲取。
這里的SecurityUtil.escapeHtmlWithoutAmpersand
和SecurityUtil.escapeJsonWithoutAmpersand
是公司安全部提供的工具包供炎,沒有條件的可以使用如下開源的替代方案:
StringEscapeUtils.escapeHtml4
這個方法來自Apache的工具類渴逻,maven坐標如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.4</version>
</dependency>
HtmlUtils.htmlEscape(value)
是來自云Spring框架,maven坐標如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
最后編寫測試程序HelloWorldController.java:
@RestController
@RequestMapping("/hello")
public class HelloWorldController {
@GetMapping("/say")
public String say(@RequestParam(name = "keyword", required = false) String keyword) {
System.out.println(">>>keyword=" + keyword);
return keyword;
}
@PostMapping("/talk1")
public String talk1(@RequestParam(value = "id", required = false) Integer id,
@RequestParam(value = "roleName", required = false) String roleName,
@RequestParam(value = "roleDes", required = false) String roleDes) {
System.out.println(">>>id=" + id + ",roleName=" + roleName + ",roleDes=" + roleDes);
return JSON.toJSONString(roleName);
}
@PostMapping("/talk2")
public String talk2(@RequestParam(value = "id", required = false) Integer id,
@RequestParam(value = "roleName", required = false) RoleName roleName,
@RequestParam(value = "roleDes", required = false) String roleDes) {
System.out.println(">>>id=" + id + ",roleName=" + roleName + ",roleDes=" + roleDes);
return JSON.toJSONString(roleName);
}
}
測試1:
測試2:
測試3:
特別說明:
通常情況下音诫,@RequestParam注解是無法直接將String轉(zhuǎn)為JavaBean對象的惨奕,如果使用X-form表單提交的數(shù)據(jù)中還有包含json格式的數(shù)據(jù),并且希望直接轉(zhuǎn)換的纽竣,可以在類中編寫一個valueOf
的方法墓贿,具體實現(xiàn)可以參考下面的示例:
@Data
public class RoleName implements Serializable {
private static final long serialVersionUID = -7166756495475187046L;
private String key1;
private String key2;
public static RoleName valueOf(String jsonValue) {
return JSON.parseObject(jsonValue, RoleName.class);
}
}
簡單做一個小結(jié),上述實現(xiàn)方式是在Spring框架體系中增加了一級XSS相關的Filter蜓氨,由于涉及到參數(shù)的修改聋袋,選擇覆寫HttpServletRequestWrapper
類來過濾和修改請求參數(shù),過濾時選擇使用到了市面上常見的一些工具類穴吹,實現(xiàn)在入?yún)⒌碾A段就對參數(shù)進行攔截和過濾幽勒,保證問題參數(shù)不會直接進入到系統(tǒng)。
2.2. 統(tǒng)一對輸出結(jié)果進行攔截和過濾
新建一個過濾器XssFilter.java
@WebFilter
@Component
public class XSSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
XssAndSqlHttpServletRequestWrapper xssRequestWrapper = new XssAndSqlHttpServletRequestWrapper(req);
chain.doFilter(xssRequestWrapper, response);
}
@Override
public void destroy() {
}
/**
* 過濾json類型的
*
* @param builder
* @return
*/
@Bean
@Primary
public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
//解析器
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
//注冊xss解析器
SimpleModule xssModule = new SimpleModule("XssStringJsonSerializer");
xssModule.addSerializer(new XssStringJsonSerializer());
objectMapper.registerModule(xssModule);
//返回
return objectMapper;
}
}
新建一個HTTP請求的包裝類XssAndSqlHttpServletRequestWrapper.java港令,來實現(xiàn)對于參數(shù)的過濾和覆寫啥容,其中xssObjectMapper這個是后面過濾json類型才用到的。
public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrapper {
private HttpServletRequest request;
public XssAndSqlHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
}
@Override
public String getParameter(String name) {
String value = request.getParameter(name);
if (!StringUtils.isEmpty(value)) {
value = StringEscapeUtils.escapeHtml4(value);
}
return value;
}
@Override
public String[] getParameterValues(String name) {
String[] parameterValues = super.getParameterValues(name);
if (parameterValues == null) {
return null;
}
for (int i = 0; i < parameterValues.length; i++) {
String value = parameterValues[i];
parameterValues[i] = StringEscapeUtils.escapeHtml4(value);
}
return parameterValues;
}
}
StringEscapeUtils.escapeHtml4這個方法來自Apache的工具類顷霹,maven坐標如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.4</version>
</dependency>
新建XssStringJsonSerializer.java實現(xiàn)自定義反序列化
/**
* @Description 這里是通過修改SpringMVC的json序列化來達到過濾xss的目的的咪惠。
* @Author louxiujun
* @Date 2020/9/5 23:40
**/
public class XssStringJsonSerializer extends JsonSerializer<String> {
@Override
public Class<String> handledType() {
return String.class;
}
@Override
public void serialize(String value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
if (value != null) {
String encodedValue = StringEscapeUtils.escapeHtml4(value);
jsonGenerator.writeString(encodedValue);
}
}
}
最后編寫一個測試類:
@RestController
@RequestMapping(value = "/test")
public class TestController {
@GetMapping(value = "/query")
public Object testQuery(@RequestParam(value = "name", required = false) String name) {
return name;
}
@PostMapping(value = "/json")
public Object testJSON(@RequestBody TestRequestParam testRequestParam) {
return testRequestParam;
}
@PostMapping(value = "/xform")
public Object xform1(@RequestParam(value = "name", required = false) String name) {
System.out.println(name);
return name;
}
}
測試1:測試get請求中的query String 中包含非法參數(shù)
測試2:測試HTTP POST 請求json格式的body體中含有非法參數(shù)
測試3:測試HTTP POST請求x-www-form-urlencoded格式的body體中含有非法參數(shù)
簡單小結(jié)一下,同樣采用了Filter過濾器和自定義Http wrapper的實現(xiàn)方法淋淀,該實現(xiàn)方式采用的是在返回結(jié)果返回給前端的做json序列化的時候進行參數(shù)的攔截和過濾遥昧,問題數(shù)據(jù)本質(zhì)上已經(jīng)存入系統(tǒng)中了。