XSS(Cross-Site-Scripting) 방지 Filter 만들기

 

XSS를 방지하기 위해 여러가지 방법이 있다.

  1. request 데이터 치환하기 (DB에 치환된 데이터로 저장)
  2. response 데이터 치환하기 (정상 데이터 저장 후 response 에서 내보내기)

회사에서 요구사항이 들어와서 개발하다 개념이 헷갈려서 정리.
1번 2번 다 만들어보고 1번으로 적용하게 되었다.

XSS 방지 Filter (Request 데이터 치환)

  • 입력 데이터에 대한 필터링을 수행할 수 있는 커스텀 필터를 구현

XSS 방지 필터 구현

public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        XssReReadableRequestWrapper wrappedRequest = new XssReReadableRequestWrapper(httpServletRequest);
        chain.doFilter(wrappedRequest, response);
    }
}

XssFilter 클래스는 Filter 인터페이스를 구현하여, 모든 요청 데이터에 대한 XSS 필터링을 수행. doFilter 메소드 내에서는 XssReReadableRequestWrapper를 사용하여 원본 HttpServletRequest를 감싸고, 요청 본문에 XSS 필터링 로직을 적용합니다.

요청 데이터를 다시 읽을 수 있는 Request Wrapper

public class XssReReadableRequestWrapper extends HttpServletRequestWrapper {

    private final Charset encoding;
    private final byte[] rawData;

    public XssReReadableRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        String characterEncoding = request.getCharacterEncoding();
        if (characterEncoding == null || characterEncoding.isEmpty()) {
            characterEncoding = StandardCharsets.UTF_8.name();
        }
        this.encoding = Charset.forName(characterEncoding);

        try (InputStream inputStream = request.getInputStream()) {
            String body = IOUtils.toString(inputStream, encoding);
            String cleanBody = cleanXss(body); // XSS 필터링 적용
            this.rawData = cleanBody.getBytes(encoding);
        } catch (IOException e) {
            throw e;
        }
    }

    private String cleanXss(String value) {
        return value.replaceAll("<", "&lt;")
                .replaceAll(">", "&gt;");
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
    }

    @Override
    public ServletRequest getRequest() {
        return super.getRequest();
    }
}

XssReReadableRequestWrapper는 HttpServletRequestWrapper를 상속받아, 요청 본문의 데이터를 필터링하고, 필터링된 데이터로 요청을 다시 구성. 입력 스트림을 한 번 읽은 후, XSS 취약점을 이용할 수 있는 문자를 안전한 문자로 치환하는 cleanXss 메소드를 통해 데이터를 치환한다.

필터 등록(WebConfig.class)

    @Bean
    public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
        FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new XssFilter());
        registrationBean.addUrlPatterns("/board/*");
        registrationBean.setOrder(1); // 필터 순서
        return registrationBean;
    }

FilterRegistrationBean을 사용하여 필터를 등록. 특정 API에만 적용하고 싶으면 registrationBean.addUrlPatterns("/board/*");를 사용한다.

XSS(JSON API 응답 데이터 보호) 방어를 위한 설정

  • JSON 응답을 보낼 때 XSS 공격을 방어

의존성 주입(gradle)

    //xss
    implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.0'
    implementation 'com.navercorp.lucy:lucy-xss:1.6.3'
    implementation 'org.apache.commons:commons-text:1.9'

XSS 공격을 방어하기 위해 네이버에서 XSS 방어를 위해 개발한 lucy-xss-servlet 및 lucy-xss 라이브러리를 사용합니다.
또한, StringEscapeUtils를 사용하기 위해(특수 문자를 HTML 엔티티로 변환하기 위해) commons-text 라이브러리를 추가.

HTMLCharacterEscapes 클래스

 public class HtmlCharacterEscapes extends CharacterEscapes {

    private final int[] asciiEscapes;

    public HtmlCharacterEscapes() {
        // 1. 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;
    }

    @Override
    public SerializableString getEscapeSequence(int ch) {
        // commons-text 라이브러리의 StringEscapeUtils 사용
        String escaped = StringEscapeUtils.escapeHtml4(Character.toString((char) ch));
        return new SerializedString(escaped);
    }
}

HtmlCharacterEscapes 클래스는 JSON 응답에서 XSS 공격에 사용될 수 있는 특수 문자를 이스케이프 처리하기 위해 정의된 클래스입니다. 이 클래스는 Jackson 라이브러리가 제공하는 CharacterEscapes를 확장하여, 특정 ASCII 코드에 대한 이스케이프 처리 규칙을 정의합니다.
StringEscapeUtils.escapeHtml4 메소드를 사용하여, 지정된 특수 문자들을 HTML 엔티티로 변환합니다.

WebConfig 설정

@Configuration
public class WebConfig implements WebMvcConfigurer {    

    private final ObjectMapper objectMapper;

    public WebConfig(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    ...

    @Bean
    public MappingJackson2HttpMessageConverter jsonEscapeConverter() {
        ObjectMapper copy = objectMapper.copy();
        copy.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
        return new MappingJackson2HttpMessageConverter(copy);
    }

jsonEscapeConverter 빈을 정의하여, 모든 JSON 응답에 대해 HtmlCharacterEscapes를 통한 이스케이프 처리를 적용.

MappingJackson2HttpMessageConverter: 이 컨버터는 Spring MVC에서 HTTP 요청 및 응답을 Java 객체와 JSON 사이에서 변환할 때 사용.커스텀 ObjectMapper를 이 컨버터에 적용함으로써, JSON으로 변환될 때 HtmlCharacterEscapes에 정의된 이스케이프 규칙을 적용한다.