오늘이군
XSS (Cross-site Scripting, 크로스 사이트 스크립팅) 방지 본문
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 시 필터링>
참조 :
- https://github.com/HomoEfficio/dev-tips/blob/master/Spring에서%20JSON에%20XSS%20방지%20처리%20하기.md
- https://jojoldu.tistory.com/470
- https://jodu.tistory.com/49
- https://lahuman.jabsiri.co.kr/155
- https://brocess.tistory.com/18
그림 : https://dev.to/maleta/cors-xss-and-csrf-with-examples-in-10-minutes-35k3
'삶.. > 프로그래밍' 카테고리의 다른 글
Redis 살펴보기 (0) | 2020.05.22 |
---|---|
@RequestBody Json 출력필터 적용 - xss (0) | 2020.05.19 |
QueryDSL multiple subselect - (a,b) in (select c,d) (0) | 2019.04.17 |
MSA (Micro service architecture) vs monolithic (0) | 2019.01.22 |
Java8 꼭 사용해야 하나요? (0) | 2019.01.04 |