"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

오늘이군

XSS (Cross-site Scripting, 크로스 사이트 스크립팅) 방지 본문

삶../프로그래밍

XSS (Cross-site Scripting, 크로스 사이트 스크립팅) 방지

오늘이군 2020. 5. 13. 17:21
반응형

1. 정의

악의적인 사용자가 공격 하려는 사이트에 스크립트를 넣는 기법을 말하며 자세한 내용은 위키를 참조하시면 좋겠습니다.

 

2. Lucy 의 한계

XSS 공격에 대한 방어를 하기 위해서 네이버에서 개발한 lucy-xss-sevlet-filter 를 사용합니다.
해당 라이브러리는 웹어플리케이션으로 들어오는 모든 요청 파라미터에 대해서 기본적으로 XSS 방어 필터링을 수행합니다.

package com.navercorp.lucy.security.xss.servletfilter;
 
public class XssEscapeServletFilter implements Filter {
 
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      chain.doFilter(new XssEscapeServletFilterWrapper(request, xssEscapeFilter), response);
   }
   
   private XssEscapeFilter xssEscapeFilter = XssEscapeFilter.getInstance();
}

<WebConfig 에 설정된 기본 Filter>

package com.navercorp.lucy.security.xss.servletfilter;

public class XssEscapeServletFilterWrapper extends HttpServletRequestWrapper {
   private XssEscapeFilter xssEscapeFilter;
 
   @Override
   public String getParameter(String paramName) {
      String value = super.getParameter(paramName);
      return doFilter(paramName, value);
   }
 
   @Override
   public String[] getParameterValues(String paramName) {
      String values[] = super.getParameterValues(paramName);
      if (values == null) {
         return values;
      }
      for (int index = 0; index < values.length; index++) {
         values[index] = doFilter(paramName, values[index]);
      }
      return values;
   }
 
   private String doFilter(String paramName, String value) { return xssEscapeFilter.doFilter(path, paramName, value); }
}

<parameter 값을 획득하는 ServletRequest의 getParameter(), getParameterValues(), getParameterMap() 호출 시 필터링>

그런데 그 처리가 form-data에 대해서만 적용되고 Request Raw Body 로 넘어가는 JSON 에 대해서는 처리해주지 않는다는 단점이 있습니다.

 

3. 출력필터

출력시 Jackson 같은 Mapper를 통해 JSON 문자열로 Response에 담겨지므로, Mapper가 JSON 문자열을 생성할 때 XSS 방지 처리를 합니다.

import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.io.SerializedString;
import org.apache.commons.lang3.StringEscapeUtils;
 
public class HtmlCharacterEscapes extends CharacterEscapes {
 
    private final int[] asciiEscapes;
 
    public HtmlCharacterEscapes() {
        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)));
    }
}
 
 
// 특수문자변환 하는데에 있어서 커스터마이징이 필요하면 CharSequenceTranslator 를 사용하여 커스터마이징 하면 됩니다. (참조 1)

<처리할 특수 문자 지정>

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

<MessageConverter에 특수문자 치환>

4. 입력필터

악의적인 공격 구문을 저장하지 않기 위해서 JSON요청의 경우 Request 를 Wrapping 하여 악의적인 공격 구문을 필터링 합니다.

public class XssEscapeServletCustomFilter implements Filter {
 
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
 
        if (isRequestBody(req)) {
            chain.doFilter(new HttpServletRequestBodyWrapper(req, xssFilter), res);
        } else {
            chain.doFilter(new XssEscapeServletFilterWrapper(req, xssEscapeFilter), res);
        }
    }
 
    private boolean isRequestBody(ServletRequest req) {
        return Objects.nonNull(req.getContentType()) && req.getContentType().contains(MediaType.APPLICATION_JSON_VALUE);
    }
 
    private XssEscapeFilter xssEscapeFilter = XssEscapeFilter.getInstance();
    private XssFilter xssFilter = XssFilter.getInstance("lucy-xss.xml");
 
    @Override public void init(FilterConfig filterConfig) {}
    @Override public void destroy() {}
}

<필터수정 - application/json 이 포함된 경우>

주의해야 할 점은 HttpServletRequestWrapper 의 getInputStream 을 Servet 의 Filter 에서 사용해버리면
다른 필터나 Controller 에서 해당 Request 에 대해 getInputStream 을 사용했을 때 참조할 수 없다는 에러가 발생하므로 HttpServletRequestWrapper 를 상속 받아 정의 합니다.

위 에러를 방지하기 위해 생성자에서 InputStream 을 읽어 전역변수 body 에 저장을 합니다. (이 때 lucy filter 를 적용하여 xss 필터링을 합니다.)
getInputStream 을 Override 해주어 전역변수 body 에 저장된 내용을 가지고 다시 InputStream 을 만들어 반환합니다.

import com.nhncorp.lucy.security.xss.XssFilter;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
 
public class HttpServletRequestBodyWrapper extends HttpServletRequestWrapper {
 
    private String body;
    private XssFilter xssFilter;
 
    public HttpServletRequestBodyWrapper(ServletRequest request, XssFilter xssFilter) throws IOException {
        super((HttpServletRequest) request);
        this.xssFilter = xssFilter;
        body = xssFilter.doFilter(
                getBody((HttpServletRequest)request)
        );
    }
 
    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream bis = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStreamImpl(bis);
    }
 
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
 
    private String getBody(HttpServletRequest request) throws IOException {
 
        StringBuilder sb = new StringBuilder();
 
        try (
                InputStream inputStream = request.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))
        ) {
            char[] charBuffer = new char[128];
            int bytesRead = -1;
            while ((bytesRead = br.read(charBuffer)) > 0) {
                sb.append(charBuffer, 0, bytesRead);
            }
        }
        return sb.toString();
    }
 
    class ServletInputStreamImpl extends ServletInputStream {
        private InputStream is;
        public ServletInputStreamImpl(InputStream bis) {
            is = bis;
        }
 
        public int read() throws IOException {
            return is.read();
        }
 
        public int read(byte[] b) throws IOException {
            return is.read(b);
        }
 
        @Override
        public boolean isFinished() {
            return false;
        }
 
        @Override
        public boolean isReady() {
            return false;
        }
 
        @Override
        public void setReadListener(ReadListener listener) {
 
        }
    }
}

<HttpServletRequestBodyWrapper>

5. multipart/form-data 입력필터

multipart/form-data 의 형식의 경우 파라미터를 읽을 때 getPart() 와 getParts() 의 메서드를 사용하는데, 
XSS filter 는 getParameter 등을 체크하므로 MultipartFilter 먼저 적용해준 뒤에 XSS filter 를 타게 해줘야 합니다. 

@Bean
public FilterRegistrationBean multiPartFilter() {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    registrationBean.setFilter(new MultipartFilter());
    registrationBean.setOrder(0);
    registrationBean.addUrlPatterns("/*");
    return registrationBean;
}
@Bean
public FilterRegistrationBean xssEscapeServletFilter() {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    registrationBean.setFilter(new XssEscapeServletCustomFilter());
    registrationBean.setOrder(1);
    registrationBean.addUrlPatterns("/*");
    return registrationBean;
}

<MultipartFilter>

 

또한 실제로 (엑셀) 파일 업로드 시 전달되는 데이터(binary) 에 대한 검증도 추가로 진행합니다.

import org.apache.poi.xssf.usermodel.XSSFCell;
 
public class ExcelUtil {
 
    private static String getStringCellValue(XSSFCell xssfCell) {
        return filterText(xssfCell.getStringCellValue());
    }
 
    private static String filterText(String dirtyText) {
        return xssEscapeFilter.doFilter(null, null, dirtyText);
    }
}

<getStringCellValue 시 필터링>

참조 :

  1. https://github.com/HomoEfficio/dev-tips/blob/master/Spring에서%20JSON에%20XSS%20방지%20처리%20하기.md
  2. https://jojoldu.tistory.com/470
  3. https://jodu.tistory.com/49
  4. https://lahuman.jabsiri.co.kr/155
  5. https://brocess.tistory.com/18

그림 : https://dev.to/maleta/cors-xss-and-csrf-with-examples-in-10-minutes-35k3

반응형

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
Comments