Home Java Servlet XSS Filter
Post
Cancel

Java Servlet XSS Filter

Java Servlet에서 XSS FIlter를 적용하면 어떤 로직에 의해서 적용되는지 알아봅시다.

File Tree

이번 실습에 사용된 트리는 다음과 같습니다.

  • InputController.java : 외부 요청 파라미터를 받아 처리한 뒤, Model 등에 담아 View로 전달하는 컨트롤러 로직
  • XssFilter.java : 요청을 가로채 원본 HttpServletRequest를 XssSanitizingRequestWrapper로 감싼 후, 변경된 요청 객체를 다음 필터 또는 컨트롤러로 전달하는 로직
  • XssSanitizingRequestWrapper.java : getParameter(), getParameterValues(), getHeader() 등을 오버라이드하여 요청값을 조회할 때 XSS 필터링 또는 HTML 인코딩을 수행하는 실제 처리 로직
  • result.jsp : 컨트롤러가 전달한 데이터를 화면에 출력하는 View 로직
  • web.xml : XSS 필터를 등록하고, 필터를 적용할 URL 패턴을 매핑하는 설정 파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─example
│  │  │          └─xssfilter
│  │  │              ├─controller
│  │  │              │      InputController.java
│  │  │              │
│  │  │              ├─filter
│  │  │              │      XssFilter.java
│  │  │              │      XssSanitizingRequestWrapper.java
│  │  │              │
│  │  │              └─model
│  │  │                      UserComment.java
│  │  │
│  │  └─webapp
│  │      └─WEB-INF
│  │          │  web.xml
│  │          │
│  │          └─views
│  │                  form.jsp
│  │                  result.jsp

코드 동작 방식

브라우저에서 파라미터 입력시 내부 동작 흐름을 나타낸 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌───────────────────────┐
│ External Entity │
│ 사용자 브라우저/클라이언트 │
└───────────┬───────────┘
│ HTTP Request (URL, QueryString, Header, Body)
▼
┌───────────────────────┐
│ Process P1 │
│ Tomcat + Servlet 엔진 │
└───────────┬───────────┘
│ web.xml 필터 매핑 검사 (/*)
▼
┌───────────────────────┐
│ Process P2 │
│ XssFilter │
│ XssFilter.java │
└───────────┬───────────┘
│ HttpServletRequest를 Wrapper로 교체
▼
┌───────────────────────┐
│ Process P3 │
│ XssSanitizingRequestWrapper │
│ XssSanitizingRequestWrapper.java │
└───────────┬───────────┘
│ getParameter / getParameterValues / getHeader 호출 시 sanitize
▼
┌───────────────────────┐
│ Process P4 │
│ InputController │
│ InputController.java │
└───────────┬───────────┘
│ 필터링된 값으로 Model 생성 후 View 전달
▼
┌───────────────────────┐
│ Process P5 │
│ JSP View (result.jsp) │
│ result.jsp │
└───────────┬───────────┘
│ HTML Response (치환된 문자열 포함)
▼
┌───────────────────────┐
│ External Entity │
│ 사용자 브라우저 │
└───────────────────────┘

코드 분석

web.xml

filter-mapping을 통해 어느 범위 까지 필터를 적용하는지 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <display-name>XSS Filter MVC Sample</display-name>

    <filter>
        <filter-name>xssFilter</filter-name>
        <filter-class>com.example.xssfilter.filter.XssFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>xssFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <welcome-file-list>
        <welcome-file>input</welcome-file>
    </welcome-file-list>
</web-app>

XssFilter.java

XssSanitizingRequestWrapper wrappedRequest = new XssSanitizingRequestWrapper(httpRequest);
위의 코드에서 httpRequest 요청을 Wrapper로 감싼 뒤 다음 로직을 수행 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.xssfilter.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class XssFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        // No initialization required for this sample.
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            XssSanitizingRequestWrapper wrappedRequest = new XssSanitizingRequestWrapper(httpRequest);
            chain.doFilter(wrappedRequest, response);
            return;
        }

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // No resources to release.
    }
}

XssSanitizingRequestWrapper.java

HttpRequest에 getParameter를 호출하고 내부에서 sanitized(value)를 수행합니다.
santized 함수 에서는 특수문자(&, <, > , “, ‘)를 HTML Entity로 escape 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.xssfilter.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class XssSanitizingRequestWrapper extends HttpServletRequestWrapper {

    public XssSanitizingRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return sanitize(value);
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) {
            return null;
        }

        String[] sanitized = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            sanitized[i] = sanitize(values[i]);
        }
        return sanitized;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return sanitize(value);
    }

    private String sanitize(String value) {
        if (value == null) {
            return null;
        }

        return value
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#x27;");
    }
}

InputController.java

namemessage 라는 파라미터를 받아 위의 XSS 필터링 로직을 수행합니다.
이후 디스패처를 통해 뷰( result.jsp )를 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.xssfilter.controller;

import com.example.xssfilter.model.UserComment;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/input")
public class InputController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        req.getRequestDispatcher("/WEB-INF/views/form.jsp").forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String name = req.getParameter("name");
        String message = req.getParameter("message");

        UserComment userComment = new UserComment(name, message);
        req.setAttribute("comment", userComment);
        req.getRequestDispatcher("/WEB-INF/views/result.jsp").forward(req, resp);
    }
}

result.jsp

외부 입력받은 파라미터는 Filter를 통한뒤 html escape로 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="com.example.xssfilter.model.UserComment" %>
<%
    UserComment comment = (UserComment) request.getAttribute("comment");
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>XSS Filter Result</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 32px; }
        .box { border: 1px solid #ccc; padding: 16px; max-width: 700px; }
        dt { font-weight: bold; margin-top: 8px; }
    </style>
</head>
<body>
<h1>Filtered Result</h1>
<div class="box">
    <dl>
        <dt>Name</dt>
        <dd><%= comment != null ? comment.getName() : "" %></dd>

        <dt>Message</dt>
        <dd><%= comment != null ? comment.getMessage() : "" %></dd>
    </dl>
</div>

<p><a href="input">Back to form</a></p>
</body>
</html>

Result

특수문자가 HTML Entity로 escape 된 것을 확인할 수 있습니다. xss

궁금한점

Filter 로직을 만들면서 궁금했던 부분들을 정리했습니다.

replace() vs replaceAll()

replace()replaceAll() 함수 모두 문자열을 치환하는 함수입니다.
처음에는 replace 첫번째 문자만, replaceAll은 모든 문자를 치환하는줄 알았는데 제가 잘못알고 있었습니다.
두 함수 모두 모든 문자를 치환하였고, 아래의 차이가 있었습니다.

  1. replace() 문자열 또는 문자 리터럴을 치환합니다.
    정규식을 사용하지 않습니다.
    대상 문자열을 모두(전역적으로) 치환합니다.
    일반적인 문자열 치환에서는 replaceAll()보다 성능이 좋습니다.
    str.replace("<", "&lt;");

  2. replaceAll() 정규식(Regex) 기반 치환입니다.
    복잡한 패턴 매칭이 필요한 경우 사용합니다.
    정규식을 컴파일하고 해석하는 과정이 있어 일반적으로 replace()보다 오버헤드가 있습니다.
    정규식 메타문자(., *, +, ( 등)를 의도하지 않게 사용하면 예상치 못한 결과가 발생할 수 있습니다.
    str.replaceAll("(?i)<\\s*script[^>]*>", "");

getParameter 와 getParameterValues 차이

XssSanitizingRequestWrapper에서 getParametergetParameterValues 함수를 사용했는데 무슨 차이가 있는지 궁금했습니다.

  • getParameter 는 같은 이름의 파라미터의 “첫 번째 값 1개를 가져옴”
  • getParameterValues 는 같은 이름의 파라미터의 “모든 값을 배열로 가져옴”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
example) : 쿼리스트링이 tag=a&tag=b&tag=c 일 때
 
 - getParameter("tag") 결과: a
 - getParameterValues("tag") 결과: [a, b, c]
 
 즉, 같은 키 tag가 3번 왔으므로:
 
 - 다중 조회는 전부
 - 단일 조회는 첫 값만
 
 예시 2
 
 쿼리스트링이 tag1=a&tag2=b&tag3=c 일 때
 
 - getParameter("tag1") 결과: a
 - getParameter("tag2") 결과: b
 - getParameter("tag3") 결과: c
 - getParameterValues("tag1") 결과: [a]
 - getParameterValues("tag2") 결과: [b]
 - getParameterValues("tag3") 결과: [c]
 - getParameter("tag") 결과: null
 - getParameterValues("tag") 결과: null
 
 즉, 키가 각각 다르면 각 키마다 값 1개짜리로 취급됩니다.

Wrapper로 감싸는 이유

Filter에서 Wrapper로 감싸는 이유는 원본 HttpServletRequest/HttpServletResponse를 수정할 수 없기 때문에, Wrapper를 통해 기능을 확장하거나 동작을 변경하기 위해서입니다.

주로 다음과 같은 목적으로 사용됩니다.

  • Request Body를 여러 번 읽기
  • 헤더, 파라미터 추가 또는 변경
  • XSS 등 입력값 필터링
  • 응답 내용 가로채기 및 수정로깅, 암호화, 압축 등 공통 기능 구현

Conclusion

최근에 XSS 취약점이 발생해서 내부 소스 코드를 분석한 결과 필터링 로직이 걸려있는데도 발생했습니다.
처음에는 필터링 로직이 이상한게 아닌가 생각했지만, 알고보니 다른 이유였습니다…..
여튼 필터링 로직이 어떻게 동작하는지 다시 한번 복기를 하기위해서 직접 실습을 해보았습니다.