Spring boot XSS Filter 적용하기 (lucy, REST Json param)

2023. 8. 17. 18:45코딩/Web

XSS는 두가지가 있습니다.

1) Reflected XSS : URL 파라미터(주로 GET)에 스크립트를 넣어 서버 저장 없이 스크립트 실행

2) Stored XSS : 게시글, 댓글 등 XSS 스크립트가 서버 DB에 저장되어, 해당 게시글/댓글 보여질때마다 스크립트 실행

 

이 글에서는 백앤드 REST API에서의 Stored XSS를 막는 XSS Filter를 적용하는 법과

Spring Security를 통해 Reflected XSS를 막는 헤더를 추가하는 법을 작성합니다.

 

최종 코드만 바로 적용하고 싶으신 분들은 아래 내용만 복사하셔서 프로젝트에 적용하시면 됩니다.

1번의 gradle과 xml코드

2번의 HtmlCharacterEscapes와 XssConfig 클래스 Java 코드

 

다른 글의 XSS Filter 소스를 적용하였지만

JSON에 대한 XSS FIlter가 적용되지 않아 이 글을 보러 오신 분들은

2번의 XssConfig 클래스의 configureMessageConverters 메서드를 추가해주시면 됩니다.

 

해당 코드들에 동작원리에 대한 부분은 최하단의 참고자료들을 보시면 더 정확하게 이해하실 수 있습니다.

 

1. Lucy 필터 적용

Lucy 필터는 네이버에서 개발된 XSS Filter입니다.

XML 설정만으로 XSS 방어가 가능해지고, 비즈니스 레이어의 코드 수정 없이 XSS 필터를 적용할 수 있습니다.

GitHub - naver/lucy-xss-servlet-filter

 

Gradle)

implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1'
implementation 'org.apache.commons:commons-text:1.10.0' // 밑에 JSON 필터에 쓰임

 

XML 코드)

resources 하단에 lucy-xss-servlet-filter-rule.xml 파일을 생성한 후 아래 내용을 작성하면 됩니다.

<?xml version="1.0" encoding="UTF-8"?>

<config xmlns="http://www.navercorp.com/lucy-xss-servlet">
  <defenders>
    <defender>
      <name>xssPreventerDefender</name>
      <class>com.navercorp.lucy.security.xss.servletfilter.defender.XssPreventerDefender</class>
    </defender>

    <defender>
      <name>xssSaxFilterDefender</name>
      <class>com.navercorp.lucy.security.xss.servletfilter.defender.XssSaxFilterDefender</class>
      <init-param>
        <param-value>lucy-xss-superset-sax.xml</param-value> 
        <param-value>false</param-value>        
      </init-param>
    </defender>

    <defender>
      <name>xssFilterDefender</name>
      <class>com.navercorp.lucy.security.xss.servletfilter.defender.XssFilterDefender</class>
      <init-param>
        <param-value>lucy-xss.xml</param-value>   
        <param-value>false</param-value>         
      </init-param>
    </defender>
  </defenders>

  <default>
    <defender>xssPreventerDefender</defender>
  </default>

  <global>
    <params>
      <param name="globalParameter" useDefender="false" />
      <param name="globalPrefixParameter1" usePrefix="true" useDefender="false" />
      <param name="globalPrefixParameter2" usePrefix="true" />
      <param name="globalPrefixParameter3" usePrefix="false" useDefender="false" />
    </params>
  </global>

  <url-rule-set>
    <!-- <url-rule>
        <url disable="true">/login/login/loginAjax</url>
    </url-rule> -->
  </url-rule-set>
</config>

 

실제 적용을 위해 XssConfig.java 파일을 만들어주고, 필터를 적용합니다.

import com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class XssConfig implements WebMvcConfigurer {

  // lucy 필터 적용
  @Bean
  public FilterRegistrationBean<XssEscapeServletFilter> filterRegistrationBean() {
    final FilterRegistrationBean<XssEscapeServletFilter> filterRegistration = new FilterRegistrationBean<>();
    filterRegistration.setFilter(new XssEscapeServletFilter());
    filterRegistration.setOrder(1);
    filterRegistration.addUrlPatterns("/*"); //filter를 거칠 url patterns
    return filterRegistration;
  }
}

 

이렇게 하면 String 등 일반적인 자료형은 XSS Filter가 적용됩니다.

 

다만 흔히 REST에서 사용되는 JSON 파라미터로 들어오는 값은 필터가 안되어서

해당 부분에 대해 추가로 XSS Filter를 작성해야합니다.

 

2. JSON 파라미터 XSS Filter 작성

XSS 방지 처리할 특수문자들을 지정하는 HtmlCharacterEscapes 클래스를 추가해줍니다.

추가로 그냥 사용할 시, 이모지가 필터링 될 수 있으므로 이모지 관련 필터 예외해주는 코드가 추가됩니다

 

import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.io.SerializedString;
import org.apache.commons.text.StringEscapeUtils;

public class HtmlCharacterEscapes extends CharacterEscapes {
  private final int[] asciiEscapes;

  public HtmlCharacterEscapes() {
    // XSS 방지 처리할 특수 문자 지정
    asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();
    asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['\"'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['('] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes[')'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['#'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;
  }

  @Override
  public int[] getEscapeCodesForAscii() {
    return asciiEscapes;
  }

  // Emoji 처리를 위한 필터 예외처리 코드 추가 적용
  @Override
  public SerializableString getEscapeSequence(int ch) {
    SerializedString serializedString = null;
    char charAt = (char) ch;
    
    //emoji jackson parse 오류에 따른 예외 처리
    if (Character.isHighSurrogate(charAt) || Character.isLowSurrogate(charAt)) {
      StringBuilder sb = new StringBuilder();
      sb.append("\\u");
      sb.append(String.format("%04x",ch));
      serializedString = new SerializedString(sb.toString());
    } else {
      serializedString = new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString(charAt)));
    }
    return serializedString;
  }
}

 

HtmlCharacterEscapes 클래스를 아까 작성한 XssConfig에 추가로 적용합니다.

스프링의 경우 Jackson을 이용하여 JSON 파라미터를 파싱하므로

Jackson에 대한 HttpMessageConverter에 방금 만든 특수문자 필터를 추가합니다.

 

그리고 configureMessageConverters 메서드로

위에 특수문자 필터를 추가한 jsonEscapeConverter를 컨버터에 추가해줍니다.

이 부분이 추가되지 않으면 실제 JSON 파라미터에 필터링이 적용되지 않을 수 있습니다.

(아래 참고자료들을 보면 해당 부분이 빠져있는 경우가 많습니다. 제가 이 글을 쓴 이유이기도 합니다 ㅠ)

import com.fasterxml.jackson.databind.ObjectMapper;
import com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class XssConfig implements WebMvcConfigurer {

  private final ObjectMapper objectMapper;

  // 방금 만든 JSON 파라미터용 필터를 실제 MessageConverters에 적용!
  // 해당 코드가 없으면 JSON 파라미터 필터링이 적용되지 않음
  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(jsonEscapeConverter());
  }

  // lucy 필터 적용
  @Bean
  public FilterRegistrationBean<XssEscapeServletFilter> filterRegistrationBean() {
    final FilterRegistrationBean<XssEscapeServletFilter> filterRegistration = new FilterRegistrationBean<>();
    filterRegistration.setFilter(new XssEscapeServletFilter());
    filterRegistration.setOrder(1);
    filterRegistration.addUrlPatterns("/*"); //filter를 거칠 url patterns
    return filterRegistration;
  }
  
  // JSON 파라미터 필터
  @Bean
  public MappingJackson2HttpMessageConverter jsonEscapeConverter() {
    ObjectMapper copy = objectMapper.copy();
    copy.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
    return new MappingJackson2HttpMessageConverter(copy);
  }
}

이후 실제 서버에서 테스트를 해보면

앞의 HtmlCharacterEscapes 클래스에 작성해둔 특수문자들이 &lt 등으로 변경되는 것을 확인하실 수 있습니다.

 

 

3. Spring Security를 통한 Xss 필터링 헤더 추가

사실 이 부분은 굳이 안해도 되긴 하는데

스프링 시큐리티에 해당 기능이 있어서 추가합니다.

 

스프링 시큐리티의 config에서 Xss Protection이라는 기능이 있습니다.

뭔가해서 확인해봤는데, 위의 stored Xss에 관련된 것은 아니고

X-XSS-Protection 이라는 헤더를 추가해줘서 Reflected XSS 를 막는다고 합니다.

다만 해당 헤더가 standard가 아니라서 모든 사용자에게 동작하지는 않는다고 합니다.

Mdn Docs 설명 : X-XSS-Protection - HTTP | MDN (mozilla.org)

 

스프링 시큐리티 Config 파일에서 아래 헤더를 추가해주면 됩니다.

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.headers()
            .xssProtection()
            .and()
            .contentSecurityPolicy("script-src 'self'");
            
        return http.build();
    }
}

 

 

 

 

참고자료)

lucy필터로 xss 방어하기(feat JSON, time, 이모지) (tistory.com)

 

lucy필터로 xss 방어하기(feat JSON, time, 이모지)

서비스를 실제로 운영할 때에 공격이 들어올 수 있고, 이를 막기 위해 노력해야 한다. 디프만에서 우리는 모든 DB통신을 JPA와 querydsl을 통해 진행했기 때문에 SQL injection은 막을 수 있었다. 그러나

hello-backend.tistory.com

Spring에서 JSON에 XSS 방지 처리 하기 - 뒤태지존의 끄적거림 (homoefficio.github.io)

 

Spring에서 JSON에 XSS 방지 처리 하기

Spring에서 JSON에 XSS 방지 처리 하기고마운 lucy-xss-servlet-filter의 한계XSS(Cross Site Scripting) 방지를 위해 널리 쓰이는 훌륭한 lucy-xss-servlet-filter는 Servlet Filter 단에서 < 등의 특수 문자를 < 등으로 변

homoefficio.github.io

 

Spring Boot에서 JSON API에 XSS Filter 적용하기 (tistory.com)

 

Spring Boot에서 JSON API에 XSS Filter 적용하기

일반적인 웹 애플리케이션에서 기본적으로 해야할 보안으로 XSS 방지가 있습니다. 기존에 많이들 알고 계시는 lucy filter의 단점은 이미 오명운 님께서 잘 정리해주셨기 때문에 한번쯤 읽어 보셔

jojoldu.tistory.com

GitHub - Spring-Boot-Course/Spring-Boot-XSS-Protection: XSS Protection at JSON

 

GitHub - Spring-Boot-Course/Spring-Boot-XSS-Protection: XSS Protection at JSON

XSS Protection at JSON. Contribute to Spring-Boot-Course/Spring-Boot-XSS-Protection development by creating an account on GitHub.

github.com

 

 

Prevent Cross-Site Scripting (XSS) in a Spring Application | Baeldung