Spring - Servlet Filter와 @ModelAttribute
이번에 내가 담당하는 사이트의 내부 보안검수를 받았는데 특정 URL에서 크로스 사이트 스크립팅(이하 XSS) 공격 위험성이 있다는 진단을 받았다. 이 웹사이트는 Spring 기반으로 작성된 프로젝트로서 XSS 방어용 Servlet Filter 를 등록해놨기 때문에 XSS 위험 딘안은 예상치 못했다.
그런데 위험 진단으로된 URL에 Mapping 되는 Controller 메서드들을 살펴보았는데 하나같이 전부 @ModelAttribute를 사용하는 곳이었다.
Why?
말 그대로 왜? 라는 의문을 가진채 일단 내가 직접 테스트 해보았더니 역시나 스크립트가 변환이 되지 않았다. 그래서 해당 메서드에 HttpServletRequest 파라미터를 받아서 값을 출력해보았다.@RequestMapping(value = "/test/xss", method = "GET")
public testXSS(@ModelAttribute Person person, HttpServletRequest request) {
// person print
// request print
}
결과가 기이하다.
request를 그대로 출력하면 변환이 되어있었고, Model로 받은 객체를 출력하면 변환이 안되어있었다.
Thinking
여기서 생각해 볼 수 있는건,- Filter가 Request 값을 변환하기전에 Model이 Request 먼저 참조해서 값을 설정한다. (타이밍의 문제)
- Filter가 변환한 Request와 Model이 참조하는 Request가 다른 객체이다. (자원 비공유의 문제)
- Filter가 잘못됐다.
일단, 변환이 되고 있긴 하므로 Filter 가 잘못된 케이스의 우선순위를 가장 아래로 내려서 생각했다.
Problem of Timing
다음은 Spring 에서 Request의 Lifecycle 이다.[ 그림1 - Spring MVC Request Lifecycle ]
보면 사용자에게서 온 Request가 제일 먼저 Filter 를 거치기 때문에 타이밍의 문제는 아닌거 같다.
Problem of Not sharing Request
사실 이미 컨트롤러단에서 HttpServletRequest 로 받았을때는 아무 문제가 없었으므로 이 가설도 무의미하다.그렇다면 @ModelAttribute가 Request에서 값을 어떻게 빼가는 걸까?
Resolve
구글 검색을 통해 구글 그룹스에 유사한 내용이 있어서 읽어보았는데 @ModelAttribute 는 request의 getParameterValues 를 통해 값을 언어온다고 한다. 관련한 Spring 문서는 찾지 못했지만 이 근거를 가지고 구현된 Filter 소스를 다시 보았다.애초에 Filter가 Request의 값을 변환한다는 표현부터가 잘못됐던건 아닐까?
Filter는 읽은 값을 변경해서 전달하는(필터링) 역할이지 기존값을 변경하는 역할이 아니다.
다음은 문제가 된 XSS Filter 소스의 일부이다.
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
chain.doFilter(doXssFiltering(req), res);
}
private HttpServletRequest doXssFiltering(HttpServletRequest req) {
...
Map<String, String[]> params = req.getParameterMap();
for (String key : params.keySet()) {
String[] values = params.get(key);
for (int i = 0; i < values.length; i++) {
values[i] = filter.doFilter(values[i]).replace("\"", """);
}
}
return req;
}
Filter 에서 Request의 getParameterMap 값만을 얻어와서 String을 변경하고 있다. 일단 getParameterMap 만을 사용하였기 때문에 보장도 안되거니와 Filter에서 직접 String 을 replace 하는건 좋지못한 방법같다.
다음과 같은 Wrapper 클래스를 만들어서 request에서 값을 읽어 들일때 변환하도록 하였다.
// Wrapper Class
class MyHttpServletRequestWrapper {
@Override
public String getParameter() {/* 변환후 리턴 */}
@Override
public String[] getParameterValues() {/* 변환후 리턴 */}
@Override
public Map<String, String[]> getParameterMap() {/* 변환후 리턴 */}
}
// Filter
...
chain.doFilter(new MyHttpServletRequestWrapper((HttpServletRequesst)req), res);
...
결과는 잘 변환되었다.