본문 바로가기

IT 개발/Spring

Spring boot 에서 XSS Filter 적용하기

반응형

 

웹 서비스 운영하려면 XSS 필터 적용은 필수다.

XSS는 크로스 사이트 스크립팅, 즉 사이트 간 스트립팅이라는 이름의 웹 취약점이다.

XSS 로 발생할 수 있는 피해로는

1. 쿠키 및 세션정보 탈취

2. 악성 프로그램 다운 유도

3. 의도하지 않은 페이지 노출

공격 구문 예시

<script>alert("hi")</script>
<scr<script>ipt>alert("hi");</scr</script>ipt>
<a onmouseover="alert('hi')">
<img src=# onerror="alert('hi')">
<ruby onmouseover="alert('hi')"></ruby>

 


대응방법

  • 입력 값의 길이 제한
  • replace 등의 함수를 이용한 치환
  • '<','>'와 같이 태그에 사용되는 기호를 엔티티코드로 변환

적용했던 예시

첫째로 StringEscapeUtils가 적용 될수 있도록 build.gradle에

implementation 'org.apache.commons:commons-text:1.10.0'

의존성을 추가한다

HtmlCharacterEscapes 클래스를 추가한다

public class HtmlCharacterEscapes extends CharacterEscapes {

private final int[] asciiEscapes;

public HtmlCharacterEscapes() {
    // 1. XSS 방지 처리할 특수 문자 지정
    asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();
    asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['\"'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['('] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes[')'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['#'] = CharacterEscapes.ESCAPE_CUSTOM;
    asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;
}

@Override
public int[] getEscapeCodesForAscii() {
    return asciiEscapes;
}

@Override
public SerializableString getEscapeSequence(int ch) {
    return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString((char) ch)));
}
}

XSS 필터 클래스를 만든다

@Component
public class XSSFilter implements Filter {

public FilterConfig filterConfig;

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

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

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

XSSFilter를 적용할 Wrapper클래스를 만든다

public class XSSFilterWrapper extends HttpServletRequestWrapper {

private byte[] rawData;

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

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

    return strData.getBytes();
}

private String replaceXSS(String value) {
    if(value != null) {
        value = value.replaceAll("\\<", "&lt;").replaceAll("\\>", "&gt;").replaceAll("\\(", "&#40;").replaceAll("\\)", "&#41;");
        value = value.replaceAll("\\'", "&apos;");
        value = value.replaceAll("\"", "&quot;");
        value = value.replaceAll("#", "&#35;");
        value = value.replaceAll("&", "&amp;");
        value = value.replaceAll("eval\\((.*)\\)", "");
        value = value.replaceAll("[\"\'][\\s]*javascript:(.*)[\"\']", "\"\"");
        value = value.replaceAll("script", "");
        value = value.replaceAll("vbscript", "");
        value = value.replaceAll("onreset", "");
        value = value.replaceAll("onmove", "");
        value = value.replaceAll("onstop", "");
        value = value.replaceAll("onrowsinserted", "");
        value = value.replaceAll("innerHTML", "");
        value = value.replaceAll("msgbox", "");
        value = value.replaceAll("onstart", "");
        value = value.replaceAll("onresize", "");
        value = value.replaceAll("onrowexit", "");
        value = value.replaceAll("onselect", "");
        value = value.replaceAll("onmousewheel", "");
        value = value.replaceAll("ondataavailable", "");
        value = value.replaceAll("onafterprint", "");
        value = value.replaceAll("onafterupdate", "");
        value = value.replaceAll("onmousedown", "");
        value = value.replaceAll("onbeforeactivate", "");
        value = value.replaceAll("ondatasetchanged", "");
        value = value.replaceAll("onbeforecopy", "");
        value = value.replaceAll("onbeforedeactivate", "");
        value = value.replaceAll("onbeforeeditfocus", "");
        value = value.replaceAll("onbeforepaste", "");
        value = value.replaceAll("onbeforeprint", "");
        value = value.replaceAll("onbeforeunload", "");
        value = value.replaceAll("onbeforeupdate", "");
        value = value.replaceAll("onpropertychange", "");
        value = value.replaceAll("ondatasetcomplete", "");
        value = value.replaceAll("oncellchange", "");
        value = value.replaceAll("onlayoutcomplete", "");
        value = value.replaceAll("onmousemove", "");
        value = value.replaceAll("oncontextmenu", "");
        value = value.replaceAll("oncontrolselect", "");
        value = value.replaceAll("onreadystatechange", "");
        value = value.replaceAll("onselectionchange", "");
        value = value.replaceAll("onactivate", "");
        value = value.replaceAll("oncopy", "");
        value = value.replaceAll("oncut", "");
        value = value.replaceAll("onclick", "");
        value = value.replaceAll("onchange", "");
        value = value.replaceAll("onbeforecut", "");
        value = value.replaceAll("ondblclick", "");
        value = value.replaceAll("ondeactivate", "");
        value = value.replaceAll("ondrag", "");
        value = value.replaceAll("ondragend", "");
        value = value.replaceAll("ondragenter", "");
        value = value.replaceAll("ondragleave", "");
        value = value.replaceAll("ondragover", "");
        value = value.replaceAll("ondragstart", "");
        value = value.replaceAll("ondrop", "");
        value = value.replaceAll("onerror", "");
        value = value.replaceAll("onerrorupdate", "");
        value = value.replaceAll("onfilterchange", "");
        value = value.replaceAll("onfinish", "");
        value = value.replaceAll("onfocus", "");
        value = value.replaceAll("onresizestart", "");
        value = value.replaceAll("onunload", "");
        value = value.replaceAll("onselectstart", "");
        value = value.replaceAll("onfocusin", "");
        value = value.replaceAll("onfocusout", "");
        value = value.replaceAll("onhelp", "");
        value = value.replaceAll("onkeydown", "");
        value = value.replaceAll("onkeypress", "");
        value = value.replaceAll("onkeyup", "");
        value = value.replaceAll("onrowsdelete", "");
        value = value.replaceAll("onload", "");
        value = value.replaceAll("onlosecapture", "");
        value = value.replaceAll("onbounce", "");
        value = value.replaceAll("onmouseenter", "");
        value = value.replaceAll("onmouseleave", "");
        value = value.replaceAll("onbefore", "");
        value = value.replaceAll("onmouseover", "");
        value = value.replaceAll("onmouseout", "");
        value = value.replaceAll("onmouseup", "");
        value = value.replaceAll("onresizeend", "");
        value = value.replaceAll("onabort", "");
        value = value.replaceAll("onmoveend", "");
        value = value.replaceAll("onmovestart", "");
        value = value.replaceAll("onrowenter", "");
        value = value.replaceAll("onsubmit", "");
        value = value.replaceAll("onblur", "");
    }
    return value;
}

//새로운 인풋스트림을 리턴하지 않으면 에러가 남
@Override
public ServletInputStream getInputStream() throws IOException {
    if(this.rawData == null) {
        return super.getInputStream();
    }

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

    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;
}
}

고객사 보안규정에 따라 금지할 단어도 추가했는데.. 이부분이 적용되는지는 확인이 더 필요할듯하다
전반적으로는 <,>,(,),*,&,/,'," 이런 특수문자에만이라도 gt,lt 이런식으로 escape가 적용된다면 xss차단이 가능하다고 보인다.

이후 WebConfig 클래스에 Bean을 추가한다

@Configuration
public class MedinaWebConfig implements WebMvcConfigurer {

private final ObjectMapper objectMapper;

public MedinaWebConfig(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
}

@Bean
public MappingJackson2HttpMessageConverter jsonEscapeConverter() {
    ObjectMapper copy = objectMapper.copy();
    copy.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
    return new MappingJackson2HttpMessageConverter(copy);
}

@Bean
public FilterRegistrationBean<XSSFilter> xssFilter() {
    FilterRegistrationBean<XSSFilter> filter = new FilterRegistrationBean<>();
    filter.setFilter(new XSSFilter());
    filter.addUrlPatterns("/*");
    return filter;
}
}
반응형