웹 애플리케이션에서 기본적으로 꼭 해야 할 보안 중 하나로 XSS 방지가 있습니다.
XSS(크로스 사이트 스크립트)란?
- 검증되지 않은 입력 값으로 인해 사용자의 웹 브라우저에서 의도하지 않은 악성 스크립트가 실행되는 취약점
- 외부 입력이 동적 웹 페이지 생성에 사용될 경우, 전송된 동적 웹 페이지를 열람하는 접속자의 권한으로 부적절한 스크립트가 수행되는 취약점
- 공격을 통해 사용자의 개인정보 및 쿠키정보 탈취, 악성코드 감염, 웹 페이지 변조 등이 발생
- 공격 대상은 서버가 아니라, 클라이언트이다.
lucy-xss-servlet-filter
lucy-xss-servlet-filter는 XSS(Cross Site Scripting)을 막기 위해 네이버에서 개발했습니다. lucy-xss-servlet-filter는 Servlet filter 기반의 라이브러리로 XSS를 쉽고 효과적으로 방어할 수 있게 해 주는데, 공식 github에서는 다음과 같이 설명하고 있습니다.
lucy-xss-servlet-filter는 웹어플리케이션으로 들어오는 모든 파라미터에 대해 기본적으로 XSS 방어 필터링을 수행하며 아래와 같은 필터링을 제외할 수 있는 효과적인 설정을 제공합니다.
lucy-xss-servlet-filter 적용
1. pom.xml에 아래 코드 적용
<dependency>
<groupId>com.navercorp.lucy</groupId>
<artifactId>lucy-xss-servlet</artifactId>
<version>2.0.0</version>
</dependency>
2. resources 디렉토리에 lucy-xss-sax.xml 파일 생성
- URL별 필터링 rule이 필요할 경우 주석 부분 설정
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.nhncorp.com/lucy-xss"
extends="lucy-xss-default-sax.xml">
<elementRule>
<element name="body" disable="true" /> <!-- <BODY ONLOAD=alert("XSS")>, <BODY BACKGROUND="javascript:alert('XSS')"> -->
<element name="embed" disable="true" />
<element name="iframe" disable="true" /> <!-- <IFRAME SRC=”http://hacker-site.com/xss.html”> -->
<element name="meta" disable="true" />
<element name="object" disable="true" />
<element name="script" disable="true" /> <!-- <SCRIPT> alert(“XSS”); </SCRIPT> -->
<element name="style" disable="true" />
<element name="link" disable="true" />
<element name="base" disable="true" />
</elementRule>
<attributeRule>
<attribute name="data" base64Decoding="true">
<notAllowedPattern><![CDATA[(?i:s\\*c\\*r\\*i\\*p\\*t\\*:)]]></notAllowedPattern>
<notAllowedPattern><![CDATA[(?i:d\\*a\\*t\\*a\\*:)]]></notAllowedPattern>
<notAllowedPattern><![CDATA[&[#\\%x]+[\da-fA-F][\da-fA-F]+]]></notAllowedPattern>
</attribute>
<attribute name="src" base64Decoding="true">
<notAllowedPattern><![CDATA[(?i:s\\*c\\*r\\*i\\*p\\*t\\*:)]]></notAllowedPattern>
<notAllowedPattern><![CDATA[(?i:d\\*a\\*t\\*a\\*:)]]></notAllowedPattern>
<notAllowedPattern><![CDATA[&[#\\%x]+[\da-fA-F][\da-fA-F]+]]></notAllowedPattern>
</attribute>
<attribute name="style">
<notAllowedPattern><![CDATA[(?i:j\\*a\\*v\\*a\\*s\\*c\\*r\\*i\\*p\\*t\\*:)]]></notAllowedPattern>
<notAllowedPattern><![CDATA[(?i:e\\*x\\*p\\*r\\*e\\*s\\*s\\*i\\*o\\*n)]]></notAllowedPattern>
<notAllowedPattern><![CDATA[&[#\\%x]+[\da-fA-F][\da-fA-F]+]]></notAllowedPattern>
</attribute>
<attribute name="href">
<notAllowedPattern><![CDATA[(?i:j\\*a\\*v\\*a\\*s\\*c\\*r\\*i\\*p\\*t\\*:)]]></notAllowedPattern>
</attribute>
</attributeRule>
</config>
3. lucy-xss-sax.xml과 같은 경로에 lucy-xss-servlet-filter-rule.xml 파일 생성
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.navercorp.com/lucy-xss-servlet">
<defenders>
<!-- XssPreventer 등록 -->
<defender>
<name>xssPreventerDefender</name>
<class>com.navercorp.lucy.security.xss.servletfilter.defender.XssPreventerDefender</class>
</defender>
<!-- XssSaxFilter 등록 -->
<defender>
<name>xssSaxFilterDefender</name>
<class>com.navercorp.lucy.security.xss.servletfilter.defender.XssSaxFilterDefender</class>
<init-param>
<param-value>lucy-xss-superset-sax.xml</param-value> <!-- lucy-xss-filter의 sax용 설정파일 -->
<param-value>false</param-value> <!-- 필터링된 코멘트를 남길지 여부, 성능 효율상 false 추천 -->
</init-param>
</defender>
<!-- XssFilter 등록 -->
<defender>
<name>xssFilterDefender</name>
<class>com.navercorp.lucy.security.xss.servletfilter.defender.XssFilterDefender</class>
<init-param>
<param-value>lucy-xss.xml</param-value> <!-- lucy-xss-filter의 dom용 설정파일 -->
<param-value>false</param-value> <!-- 필터링된 코멘트를 남길지 여부, 성능 효율상 false 추천 -->
</init-param>
</defender>
</defenders>
<!-- default defender 선언, 필터링 시 지정한 defender가 없으면 여기 정의된 default defender를 사용해 필터링 한다. -->
<default>
<defender>xssPreventerDefender</defender>
</default>
<!-- global 필터링 룰 선언 -->
<global>
<!-- 모든 url에서 들어오는 globalParameter 파라메터는 필터링 되지 않으며
또한 globalPrefixParameter1로 시작하는 파라메터도 필터링 되지 않는다.
globalPrefixParameter2는 필터링 되며 globalPrefixParameter3은 필터링 되지 않지만
더 정확한 표현이 가능하므로 globalPrefixParameter2, globalPrefixParameter3과 같은 불분명한 표현은 사용하지 않는 것이 좋다. -->
<params>
<param name="globalParameter" useDefender="false" />
<param name="globalPrefixParameter1" usePrefix="true" useDefender="false" />
<param name="globalPrefixParameter2" usePrefix="true" />
<param name="globalPrefixParameter3" usePrefix="false" useDefender="false" />
</params>
</global>
<!-- url 별 필터링 룰 선언 -->
<url-rule-set>
<!-- url disable이 true이면 지정한 url 내의 모든 파라메터는 필터링 되지 않는다. -->
<!-- <url-rule>
<url disable="true">/login/login/loginAjax</url>
</url-rule> -->
</url-rule-set>
</config>
4. ServletFilterBean을 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<XssEscapeServletFilter> filterRegistrationBean() {
FilterRegistrationBean<XssEscapeServletFilter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setFilter(new XssEscapeServletFilter());
filterRegistration.setOrder(1);
filterRegistration.addUrlPatterns("/*");
return filterRegistration;
}
}
lucy-xss-servlet-filter의 한계
lucy-xss-servlet-filter는 form-data에만 적용되고, Request Raw Body로 넘어가는 JSON에 대해서는 처리해주지 않는다는 단점이 있습니다. 그래서 JSON을 주고받아야 하는 경우에는 직접 처리를 해야 합니다.
즉, lucy 필터는 form-data 전송 방식에는 유효하지만, @RequestBody로 전달되는 JSON 요청은 처리해주지 않습니다.
해결책
1. XSS 방지 처리할 특수문자를 다음과 같이 CharacterEscapes를 상속한 클래스를 직접 만들어서 지정해줍니다.
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.text.translate.AggregateTranslator;
import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.EntityArrays;
import org.apache.commons.lang3.text.translate.LookupTranslator;
public class HTMLCharacterEscapes extends CharacterEscapes {
private final int[] asciiEscapes;
private final CharSequenceTranslator translator;
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;
asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;
// 2. XSS 방지 처리 특수 문자 인코딩 값 지정
translator = new AggregateTranslator(
new LookupTranslator(EntityArrays.BASIC_ESCAPE()),// <, >, &, " 는 여기에 포함됨
new LookupTranslator(EntityArrays.ISO8859_1_ESCAPE()),
new LookupTranslator(EntityArrays.HTML40_EXTENDED_ESCAPE()),
// 여기에서 커스터마이징 가능
new LookupTranslator(
new String[][]{
{"(", "("},
{")", ")"},
{"#", "#"},
{"\'", "'"}
}
)
);
}
@Override
public int[] getEscapeCodesForAscii() {
return asciiEscapes;
}
@Override
public SerializableString getEscapeSequence(int ch) {
return new SerializedString(translator.translate(Character.toString((char) ch)));
// 참고 - 커스터마이징이 필요없다면 아래와 같이 Apache Commons Lang3에서 제공하는 메서드를 써도 된다.
// return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString((char) ch)));
}
}
2. StringEscapeUtils를 사용하기 위해 commons-text 의존성을 추가합니다.(gradle 기준)
compile('org.apache.commons:commons-text:1.8')
3. 아래의 config 클래스를 추가해서 사용합니다.
@Slf4j
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig {
private final ObjectMapper objectMapper;
@Bean
public MappingJackson2HttpMessageConverter jsonEscapeConverter() {
ObjectMapper copy = objectMapper.copy();
copy.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
return new MappingJackson2HttpMessageConverter(copy);
}
}
참고자료
'웹 개발' 카테고리의 다른 글
Spring boot] json 형식으로 return 시, date format (0) | 2020.12.13 |
---|---|
Spring boot] HTTP Request Interceptor 추가 (0) | 2020.12.13 |
log4j로 printStackTrace 출력 (0) | 2020.12.12 |
eclipse] maven build 시 test를 빼는 방법 (0) | 2020.12.06 |
StringBuffer와 StringBuilder (0) | 2020.12.05 |