Spring 4로 개인 블로그 툴을 만들던 중, CKEditor source 모드에서 스크립트를 입력하고 저장했더니 XSS가 그대로 실행되는 문제가 발생했다.


이런...😱
예전에는 <script>를 <script> 로 바꿔주는 메서드를 만들어 비즈니스 로직마다 일일이 집어넣었으나, 이번에는 Filter로 일괄 처리하기로 한다.
1. 사용 라이브러리
네이버에서 만든 lucy-xss-filter 라이브러리를 적용하여 HttpServletRequest를 통해 받아오는 인자들을 Escape 처리한다.
2. HttpServletRequestWrapper 확장 클래스 구현
HttpServletRequest의 파라미터 값을 가져온 뒤 Escape 처리한 값을 동일한 key로 리턴하기 위해 HttpServletRequestWrapper 를 상속한 확장 클래스를 생성한다.
getParameter, getParameterValues, getHeader 등 값을 가져오는 메서드를 오버라이드하여 필터 적용 결과를 리턴하도록 구현한다.
import com.nhncorp.lucy.security.xss.XssFilter;
import com.nhncorp.lucy.security.xss.XssPreventer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.Arrays;
public class RequestWrapperForXssFiltering extends HttpServletRequestWrapper {
public RequestWrapperForXssFiltering(HttpServletRequest httpServletRequest) {
super(httpServletRequest);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
boolean doPreventer = getPreventerFlag(name);
return Arrays.stream(values)
.map(v -> doFilter(v, doPreventer))
.toArray(String[]::new);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value == null) return null;
return doFilter(value, getPreventerFlag(name));
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (value == null) return null;
return doFilter(value, true);
}
private String doFilter(String value, boolean doPreventer) {
if (doPreventer) {
return XssPreventer.escape(value);
} else {
XssFilter xssFilter = XssFilter.getInstance("lucy-xss-config.xml");
return xssFilter.doFilter(value);
}
}
private Boolean getPreventerFlag(String name) {
if (name == null) return true;
return !name.toLowerCase().startsWith("content");
}
}
Lucy-XSS 라이브러리의 두 가지 기능
| 기능 | 적용 대상 | 동작 |
|---|---|---|
| XssPreventer | 단순 문자열 파라미터 | 전체 값을 Escape 처리 |
| XssFilter | HTML 태그가 필요한 본문(content*) |
스크립트 실행 등만 선별적으로 필터링 |
getPreventerFlag 메서드에서 파라미터명이 content로 시작하는 경우를 HTML 본문으로 간주하여 XssFilter를 적용하고, 그 외에는 XssPreventer를 적용한다.
참고: content로 하드코딩한 부분은 개인 프로젝트이므로 일단 넘어간다. 😅
3. XssFilter 설정 파일
Lucy-XSS Filter는 XML 파일로 정의된 필터링 정책에 의거하여 구동된다.
lucy-xss-superset.xml,white-url.xml,lucy-xss.xml파일을lucy-xss-config.xml로 통합하여 루트 디렉터리에 배치- XML 파일 경로가 잘못되거나 없으면 라이브러리 내 기본 설정(필터링 레벨 낮음)을 읽어들임

4. Filter 클래스 구현
Spring의 OncePerRequestFilter를 상속받아 Filter 클래스를 구현한다.
HTTP method가 GET 또는 POST일 경우에만 XSS Filter가 적용되도록 한다.
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CrossSiteScriptingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
if (httpServletRequest.getMethod().equals("GET")
|| httpServletRequest.getMethod().equals("POST")) {
filterChain.doFilter(
new RequestWrapperForXssFiltering(httpServletRequest),
httpServletResponse
);
} else {
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
5. web.xml에 Filter 등록
만든 Filter 클래스를 web.xml에 추가한다.

6. 결과 확인
서버 재기동 후 스크립트를 포함하여 테스트 입력하면 아래와 같이 스크립트가 Escape 처리되어 동작하지 않고 DB에 정상적으로 저장된다.
