XSS를 방지하기 위해 여러가지 방법이 있다.
- request 데이터 치환하기 (DB에 치환된 데이터로 저장)
- 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("<", "<")
.replaceAll(">", ">");
}
@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
에 정의된 이스케이프 규칙을 적용한다.