프로그램/JAVA

[Filter] Spring Filter XSS 공격 방어하기 (ResponseFilter)

프뚜 2023. 1. 6. 10:00
728x90
반응형
SMALL

안녕하세요! 프뚜입니다.

 

이전에는 RequestFilter를 통해서 XSS 처리 후 데이터베이스에 insert, update등을 했습니다. 하지만 XSS 처리가 된 데이터를 비지니스로직에서 사용해야할 때가 발생되었습니다. 이후 RequestFilter가 아닌 ResponseFilter에서 처리하게 되었습니다.

 


[개발 환경]

 - OS: Windows 10 64bit

 - JAVA: 1.8


# XSS Filter Class 생성하기

@Component
public class XSSFilter implements Filter {

    // 2022.12.26[프뚜]: path 제외시킬 URI
    private String[] excludePathPatterns = {
        "/exclude1/*",
        "/exclude2/**/double/*"
    };

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 2022.12.26[프뚜]: 정규식을 통해 excludePath 문자를 변경
        String asterisk = "([^/]+)";
        String doubleAsterisk = "(.+)";

        for (int i = 0; i < excludePathPatterns.length; i++) {
            excludePathPatterns[i] = excludePathPatterns[i].replaceAll("[*][*]", doubleAsterisk)
                                                .replaceAll("[*]", asterisk);
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        System.out.println("[LOG] URI: " + httpServletRequest.getRequestURI());

        if (isExcludePath(httpServletRequest)) {
            chain.doFilter(request, response);
        } else {
            // 2022.12.26[프뚜]: Response Wrapper
            XSSFilterWrapper responseWrapper = new XSSFilterWrapper(httpServletResponse);
            chain.doFilter(request, responseWrapper);

            // 2022.12.26[프뚜]: 최종 데이터 처리
            byte[] bytes = responseWrapper.replaceXSS();
            int byteLength = bytes.length;
            if (byteLength > 0) {
                response.setContentLength(byteLength);
                response.getOutputStream().write(bytes);
                response.flushBuffer();
            }
        }
    }

    private boolean isExcludePath(HttpServletRequest httpServletRequest) {
        for (String pattern : excludePathPatterns) {
            // 2022.12.26[프뚜]: 정규식으로 변한 path를 Pattern.matches를 통해 비교
            if (Pattern.matches(pattern, httpServletRequest.getRequestURI())) {
                return true;
            }
        }

        return false;
    }

}

필터에 모든 URI가 적용되기 때문에 excloude할 수 있는 메서드를 구현했습니다. (정규표현식 사용)

 - *: /를 제외한 어떠한 문자도 허용

 - **: 모든 문자도 허용(여러 뎁스의 URI도 허용)


# XSSFilterWrapper Class 생성하기

@Component
public class XSSFilterWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream byteArrayOutputStream;
    private ServletOutputStream servletOutputStream;

    public XSSFilterWrapper(HttpServletResponse httpServletResponse) {
        super(httpServletResponse);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (servletOutputStream == null) {
            byteArrayOutputStream = new ByteArrayOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
            servletOutputStream = new ServletOutputStream() {

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

                @Override
                public void setWriteListener(WriteListener listener) {

                }

                @Override
                public void write(int b) throws IOException {
                    dataOutputStream.write(b);
                }
            };
        }

        return servletOutputStream;
    }

    public byte[] replaceXSS() {
        // 2022.12.26[프뚜]: XSS 처리
        return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)
                .replaceAll("\\<", "&lt;")
                .replaceAll("\\>", "&gt;")
                .replaceAll("\"", "&quot;")
                .replaceAll("'", "&#39;")
                .getBytes(StandardCharsets.UTF_8);
    }

}

response의 모든 데이터에 대해 XSS 처리를 합니다.


# TestController Class 생성하기

@RequiredArgsConstructor
@RestController
public class TestController {

    @GetMapping("/")
    public ResponseEntity<?> main() throws Exception {
        return ResponseEntity.ok().body("main");
    }

    @PostMapping("/test")
    public ResponseEntity<?> test(@RequestBody Map param) throws Exception {
        return ResponseEntity.ok().body(param);
    }

    @PostMapping("/exclude1/test")
    public ResponseEntity<?> exclude1(@RequestBody Map param) throws Exception {
        return ResponseEntity.ok().body(param);
    }

    @PostMapping("/exclude2/t/e/s/t/double/test")
    public ResponseEntity<?> exclude2(@RequestBody Map param) throws Exception {
        return ResponseEntity.ok().body(param);
    }

}

정상적으로 적용되었는 지와 exclude가 정상적으로 되는 지 확인할 수 있는 RequestMapping을 합니다.


# Postman으로 테스트하기

/test (POST)의 결과 XSSFilter가 정상적용됨을 확인할 수 있습니다.

 

 

/exclude1/test (POST)의 결과 XSSFilter적용이 되지 않음을 확인할 수 있습니다. excludePath에 /exclude1/*로 되어있었기 때문에 위 URI는 Filter적용이 되지 않았습니다.

 

 

/exclude2/t/e/s/t/double/test (POST)의 결과 XSSFilter적용이 되지 않음을 확인할 수 있습니다. excludePath에 /exclude2/**/double/*로 되어있었기 때문에 위 URI는 Filter적용이 되지 않았습니다.


자세한 내용은 소스코드를 통해 확인해보실 수 있습니다. (제목과 Git Comment는 같습니다.)

https://github.com/JeongSeongSoo/spring-tistory

 

GitHub - JeongSeongSoo/spring-tistory

Contribute to JeongSeongSoo/spring-tistory development by creating an account on GitHub.

github.com

728x90
반응형
LIST