Skip to content
Rhys Chang edited this page Aug 4, 2022 · 3 revisions

A lightweight set of escaping routines for cross-site scripting (XSS) in SoftLeader web applications.

X-XSS-Protection

預設開啟 spring security 的 xss protection, 會在每次 response 的 header 中加上下列 header, 讓有支援的瀏覽器來協助防範 xss 攻擊

X-XSS-Protection: 1; mode=block

see X-XSS-Protection

若專案需要客製設定, 可參考下述程式碼

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends MoreWebSecurityConfiguration {

  ...
	
  @Override
  protected void headers(HeadersConfigurer<HttpSecurity> headers) {
      super.headers(headers);
      // headers.xssProtection().disable(); // 關掉
      headers.xssProtection().block(false); // 不設定 mode=block 
  }
}

Filter

註冊 XSSProtectionFilter 針對 HttpServletRequest 提供了跳脫 xss 的裝飾實作

Enable

在專案的 WebApplicationInitializer 加上 filter 即完成設定

public class WebApplicationInitializer extends ProfileAnnotationConfigDispatcherServletInitializer {

  ...
  
  @Override
  protected Filter[] getServletFilters() {
    return new Filter[] {
      new org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter(),
      new HtmlXSSProtectionFilter()
    };
  }
}

跳脫的實作

核心提供了 HtmlXSSProtectionFilter 的實作, 是使用 Spring 的 HtmlUtils.htmlEscape(String) 來跳脫

如果專案想客製 , 可以改使用上層的 XSSProtectionFilter 來自行實作跳脫邏輯

@Override
protected Filter[] getServletFilters() {
  return new Filter[] {
    new org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter(),
    new XSSProtectionFilter(original -> {
      ...customize your escaping logic
      return escaped;
    })
  };
}

或接續核心的 HtmlEscaper 繼續跳脫

new XSSProtectionFilter(new HtmlEscaper().andThen(original -> {
  ...customize more escaping logic
  return escaped;
}))

跳脫 method

加上 filter 後, 任何從 HttpServletRequest 呼叫下述 method 取回的值, 都會自動跳脫:

  • String getParameter(String)
  • Map<String, String[]> getParameterMap()
  • String[] getParameterValues(String)
@RestController
@RequestMapping("/some-path")
public class SomeController {

  public void escape(HttpServletRequest request) {
    String escaped = request.getParameter(..);
    ...
  }
}

取得未跳脫前的狀態

若有需求可以參考下述程式碼, 取得尚未跳脫前的值

@RestController
@RequestMapping("/some-path")
public class SomeController {

  public void escape(HttpServletRequest request) {
    String beforeEscape =
        ((XSSHttpServletRequest) request)
            .getNativeRequest(HttpServletRequest.class)
            .map(nativeRequest -> nativeRequest.getParameter(...))
            .orElseThrow(/** handle if not available **/);
    ...
  }
}

@RequestParam

也支援 spring 提供的 @RequestParam 取值方式

@RestController
@RequestMapping("/some-path")
public class SomeController {

  public void escape(@RequestParam(...) String escaped) {
    ...
  }
}

注意: 使用 @RequestParam 將無法再拿到跳脫前的值,且如果宣告的是物件, spring 會以跳脫後的值進行轉換

Jackson

註冊 XSSProtectionModule 會針對所有 String 欄位在 deserialize 時自動跳脫

public class WebMvcConfig extends WebMvcConfiguration {

  ...
    
  @Override
  public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
    super.addArgumentResolvers(argumentResolvers);    
    ...
    argumentResolvers.add(new SpecificationArgumentResolver(new Converter(objectMapper())));
  }

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    super.configureMessageConverters(converters);
    ...    
    converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
  }
  
  @Bean
  public ObjectMapper objectMapper() {
    return Jackson2ObjectMapperBuilder
        .json()
        .modules(new HtmlXSSProtectionModule())
        .build();
  }
}

如果專案想客製, 可以改使用上層的 XSSProtectionModule 來自行實作客製邏輯

new XSSProtectionModule(original -> {
  ...customize your escaping logic
  return escaped;
});

如果專案在 spring 中已經定義了一個 global 的 object mapper, 建議將 XSSProtectionModule 的跳脫範圍限制在 request 進 controller 轉換使用, 以避在任何地方只要用到 object mapper 來轉換時就會跳脫, 讓範圍太大較難以控制

public class WebMvcConfig extends WebMvcConfiguration {

  ...

  @Override
  public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
    super.addArgumentResolvers(argumentResolvers);    
    ...
    argumentResolvers.add(new SpecificationArgumentResolver(new Converter(xssObjectMapper())));
  }
  
  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    super.configureMessageConverters(converters);
    ...
    converters.add(new MappingJackson2HttpMessageConverter(xssObjectMapper()));
  }
  
  // 將原本的 mapper 設定成 @Primary 讓一般程式使用
  @Primary
  @Bean
  public ObjectMapper objectMapper() {
    return new MyObjectMapper();
  }
  
  // 跟處理 request 有關係的程式才加上 XSSProtectionModule 
  @Bean
  public ObjectMapper xssObjectMapper() {
    MyObjectMapper mapper = new MyObjectMapper();
    mapper.registerModule(new HtmlXSSProtectionModule());
    return mapper;
  }
}

Logging level

如果想看到每次跳脫前後的值, 則把下述 class 的 log level 設在 debug

  • tw.com.softleader.security.xss.http.XSSHttpServletRequest
  • tw.com.softleader.security.xss.json.jackson.XSSStringDeserializer

設定範例

核心提供的實作預設都是使用 Spring 的 HtmlUtils.htmlEscape(String) 來跳脫,如果專案想要調整 (如改成 OWASP),則可以參考以下範例:

  • pom 加上 owasp 依賴
<dependency>
    <groupId>tw.com.softleader</groupId>
    <artifactId>softleader-security-xss</artifactId>
    <version>1.0.4-SNAPSHOT</version>
</dependency>
  • pom 加上 owasp 依賴
<dependency>
    <groupId>org.owasp.encoder</groupId>
    <artifactId>encoder</artifactId>
    <version>1.2.1</version>
</dependency>
  • xss filter 改成使用 org.owasp.encoder.Encode.forHtml(String) 跳脫
public class WebApplicationInitializer extends ProfileAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Filter[] getServletFilters() {
    return new Filter[]{new OpenEntityManagerInViewFilter(), new XSSProtectionFilter(Encode::forHtml)};
  }

  // ...  
}
  • xss module 也是改使用 Encode.forHtml(String) 跳脫
@Configuration
public class SomeConfig {

  ...

  @Bean
  public ObjectMapper xssObjectMapper() {
    MyObjectMapper mapper = new MyObjectMapper();
    mapper.registerModule(new XSSProtectionModule(Encode::forHtml));
    return mapper;
  }
}