XSS 공격 대응 방법 [Spring]

M.S·2024년 6월 21일
  1. lucy-xss-servlet-filter의 한계
    - lucy 필터를 적용했지만 body쪽에 json 형태로 들어오는 xss 공격은 적용이 되지 않았다.
    • 그래서 찾아보니 filter로 InputStream을 읽어와서 변경 해주는 방식이 있어서 적용해봤다.
    • 하지만 InputStream의 경우 한번 읽으면 다시 읽을 수 없었다. => tomcat이 이것을 막아놨다.
    • 따라서 wrapper를 만들어 InputStream을 읽어서 다시 돌려주는 방식으로 진행했다.

  2. web.xml에 filter 추가
    • 해당 filter는 CharacterEncodingFilter 보다 밑에 선언 해주는게 좋다.
    <filter>
        <filter-name>XSS</filter-name>
        <!-- 해당 필터 클래스 파일 위치 -->
        <filter-class>com.example.common.XSSFilter</filter-class>        
    </filter>    
    <filter-mapping>
        <filter-name>XSS</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

  1. XSSFilter 클래스 생성
public class XSSFilter implements Filter {

	 private FilterConfig filterConfig;

	 @Override
	 public void init(FilterConfig filterConfig) throws ServletException {
	  	this.filterConfig = filterConfig;
	 }

	 @Override
	 public void destroy() {
	  this.filterConfig = null;
	 }

	 @Override
	 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		 chain.doFilter(new XSSFilterWrapper((HttpServletRequest) request), response);
	 }
}

  1. XSSFilterWrapper 클래스 생성
    • 앞에서 말했듯이 InputStream의 경우 한번 읽으면 다시 읽기가 불가능 하므로 wrapper 객체를 만들어 InputStream을 읽어서 다시 돌려주는 방식 사용
    • 하지만 필자는 밑에 코드처럼 인터넷에서 본대로 따라하니까 InputStream의 데이터를 읽어오고 변환하는 과정까지는 잘 됐는데 @Requestparam Map에서 받아오던 데이터를 받아오는게 안되서 5번처럼 변경했다.
    • 그랬더니 잘 동작하는걸 확인했다. 휴 어렵다.
public class XSSFilterWrapper extends HttpServletRequestWrapper {    

    private byte[] requestBody;

    public XSSFilterWrapper(HttpServletRequest request) {
        super(request);
        
        try {
            InputStream inputStream = request.getInputStream();
            this.requestBody = replaceXSS(IOUtils.toByteArray(inputStream));
        } catch (Exception e) {
			e.printStackTrace();
        }
    }

    private byte[] replaceXSS(byte[] data) {
        String strData = new String(data);
        strData = strData.replaceAll("\\<", "&lt;").replaceAll("\\>", "&gt;").replaceAll("\\(", "&#40;").replaceAll("\\)", "&#41;");
        byte[] byteData = strData.getBytes();

        return byteData;
    }

    private String replaceXSS(String value) {
        if(value != null) {
            value = value.replaceAll("\\<", "&lt;").replaceAll("\\>", "&gt;").replaceAll("\\(", "&#40;").replaceAll("\\)", "&#41;");
        }
        return value;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if ( this.requestBody == null ) {
            return super.getInputStream();
        }

        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.requestBody);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

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

            @Override
            public boolean isFinished() {
                return false;
            }
        };
    }

    @Override
    public String getQueryString() {
        return replaceXSS(super.getQueryString());
    }

    @Override
    public String getParameter(String name) {
        return replaceXSS(super.getParameter(name));
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> params = super.getParameterMap();

        if(params != null) {
            params.forEach((key, value) -> {
                for(int i=0; i < value.length; i++) {
                    value[i] = replaceXSS(value[i]);
                }
            }) ;
        }
        return params;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] params = super.getParameterValues(name);
        if(params != null) {
            for(int i=0; i<params.length; i++) {
                params[i] = replaceXSS(params[i]);
            }
        }
        return params;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), "UTF-8"));
    }
}

  1. XSSFilterWrapper 변경
public class XSSFilterWrapper extends HttpServletRequestWrapper {

    private byte[] requestBody;
    private boolean hasReadRequestBody = false;

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

    private void readRequestBody() {
        if (!hasReadRequestBody) {
            try {
                InputStream inputStream = super.getInputStream();
                this.requestBody = replaceXSS(IOUtils.toByteArray(inputStream));
                hasReadRequestBody = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private byte[] replaceXSS(byte[] data) {
        String strData = new String(data);
        strData = strData.replaceAll("\\<", "&lt;").replaceAll("\\>", "&gt;").replaceAll("\\(", "&#40;").replaceAll("\\)", "&#41;");
        byte[] byteData = strData.getBytes();

        return byteData;
    }

    private String replaceXSS(String value) {
        if(value != null) {
            value = value.replaceAll("\\<", "&lt;").replaceAll("\\>", "&gt;").replaceAll("\\(", "&#40;").replaceAll("\\)", "&#41;");
        }
        return value;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        readRequestBody();
        if ( this.requestBody == null ) {
            return super.getInputStream();
        }

        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.requestBody);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

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

            @Override
            public boolean isFinished() {
                return false;
            }
        };
    }

    @Override
    public String getQueryString() {
        return replaceXSS(super.getQueryString());
    }

    @Override
    public String getParameter(String name) {
        return replaceXSS(super.getParameter(name));
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> params = super.getParameterMap();

        if(params != null) {
            params.forEach((key, value) -> {
                for(int i=0; i < value.length; i++) {
                    value[i] = replaceXSS(value[i]);
                }
            }) ;
        }
        return params;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] params = super.getParameterValues(name);
        if(params != null) {
            for(int i=0; i<params.length; i++) {
                params[i] = replaceXSS(params[i]);
            }
        }
        return params;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), "UTF-8"));
    }
}

  • 마지막으로 필자는 <, >, (, ) 정도만 구현해도 되서 이것만 있지만 다른 스크립트 기호나 함수들은 인터넷에 많으니 찾아서 추가하시면 될거 같습니다.
    제가 찾아보면서 본 패턴들은 밑에 더 추가 해드릴게요.
    private static Pattern[] patterns = new Pattern[] {
            Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
            Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
            Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
            Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
            Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
    };

    private String stripXSS(String value) {
        if(value != null) {
            value = value.replaceAll("\0", "");

            for(Pattern scriptPattern : patterns){
                if(scriptPattern.matcher(value).matches()){
                    value = value.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
                }
            }
            value = value.replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("'","&apos;");
        }
        return value;
    }

참고 자료
1. https://velog.io/@ch200203/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-XSS-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
2. https://hello-backend.tistory.com/168

profile
나만의 메모장 같은 기록

0개의 댓글