반응형
웹 서비스 운영하려면 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("\\<", "<").replaceAll("\\>", ">").replaceAll("\\(", "(").replaceAll("\\)", ")");
return strData.getBytes();
}
private String replaceXSS(String value) {
if(value != null) {
value = value.replaceAll("\\<", "<").replaceAll("\\>", ">").replaceAll("\\(", "(").replaceAll("\\)", ")");
value = value.replaceAll("\\'", "'");
value = value.replaceAll("\"", """);
value = value.replaceAll("#", "#");
value = value.replaceAll("&", "&");
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("<", "<").replaceAll(">", ">");
}
}
value = value.replaceAll("<", "<").replaceAll(">", ">").replaceAll("'","'");
}
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;
}
}
반응형
'IT 개발 > Spring' 카테고리의 다른 글
AOP를 사용한 로그 DB에 저장(코드포함) (0) | 2024.07.09 |
---|---|
@Scheduled Open API사용하여 공휴일 DB 적재하기 (1) | 2023.12.05 |
AOP를 활용하여 로그 기록하기 (0) | 2023.12.01 |
WebConfig에 설정된 경로 중 일부 API 예외처리 (2) | 2023.12.01 |