XSS(Cross Site Scripting)
시작하기
lucy-xss-servlet-filter
XSS(Cross Site Scripting) 방지를 위해 널리 쓰이는 lucy-xss-servlet-filter는 Servlet Filter 단에서
<
등의 특수 문자를 <
등으로 변환해주며, 여러 가지 관련 설정을 편리하게 지정할 수 있습니다.
index 페이지
Controller
// src/main/java/com/jjamong/xss/controller/IndexController.java
package com.jjamong.xss.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class IndexController {
@RequestMapping("/")
public String index() {
return "index";
}
@RequestMapping(value = "/insert", method = RequestMethod.POST)
public String insert(@RequestParam("text1") String text1, @RequestParam("text2") String text2) {
System.out.println(">>> text1 : " + text1);
System.out.println(">>> text2 : " + text2);
return "redirect:/";
}
}
jsp
// src/main/webapp/WEB-INF/jsp/index.jsp
<form action="/insert" method="post">
<input type="text" name="text1" value="alert(1)" />
<input type="text" name="text2" value="<script>alert(1)</script>" />
<button type="submit">insert</button>
</form>
실행
여기서 insert 버튼을 누르면 아래와 같이 url 파라미터로 스크립트 코드가 전송되며
XSS 취약점이 발견된 것을 확인할 수 있습니다.
>>> text1 : alert(1)
>>> text2 : <script>alert(1)</script>
lucy 의존성 모듈 설정
// build.gradle
implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.0'
XSS 필터 적용
// src/main/java/com/jjamong/xss/config/WebMvcConfig.java
package com.jjamong.xss.config;
import com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebMvcConfig extends WebMvcAutoConfiguration {
//Lucy Xss filter 적용
@Bean
public FilterRegistrationBean<XssEscapeServletFilter> getFilterRegistrationBean(){
FilterRegistrationBean<XssEscapeServletFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssEscapeServletFilter());
registrationBean.setOrder(1);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
XSS 필터 룰 적용
// src/main/resources/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>
</defenders>
<!-- default defender 선언, 별다른 defender 선언이 없으면 default defender를 사용해 필터링 한다. -->
<default>
<defender>xssPreventerDefender</defender>
</default>
</config>
실행
다시 insert 버튼을 선택 하면 아래와 같이
스크립트 코드가 변환되어서 전송된 것을 확인 할 수 있습니다.
>>> text1 : alert(1)
>>> text2 : <script>alert(1)</script>
Jackson
lucy-xss-servlet-filter는 JSON에 대한 XSS는 처리해 주지 않는다는 한계가 있습니다.
form-data 에 대해서만 적용 되고 Request Raw Body에 대해서는 처리해 주지 않습니다.
그래서 JSON 형태에 값에 대해선 직접 처리를 해야 합니다.
Jackson의 com.fasterxml.jackson.core.io.CharacterEscapes를 상속하는 클래스를 직접 만들어서 처리해야 할 특수문자를 지정하고,
ObjectMapper에 만든 클래스를 설정하고
ObjectMapper를 MessageConverter에 등록해서 Response가 클라이언트에 나가기 전에 XSS 방지 처리 해줍니다.
lucy 의존성 모듈 설정
// build.gradle
implementation 'org.apache.commons:commons-text:1.8'
HtmlCharacterEscapes
package com.jjamong.xss.util;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.io.SerializedString;
import org.apache.commons.text.StringEscapeUtils;
public class HtmlCharacterEscapes extends CharacterEscapes {
private final int[] asciiEscapes;
public HtmlCharacterEscapes() {
this.asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();
this.asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['\"'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['('] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes[')'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['#'] = CharacterEscapes.ESCAPE_CUSTOM;
this.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)));
}
public String unitChange(String text) {
HtmlCharacterEscapes()
return
}
}
WebMvcConfig 설정
package com.jjamong.xss.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jjamong.xss.util.HtmlCharacterEscapes;
@Configuration
public class WebMvcConfig {
private final ObjectMapper objectMapper;
@Autowired
public WebMvcConfig(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Bean
public MappingJackson2HttpMessageConverter jsonEscapeConverter() {
ObjectMapper copy = objectMapper.copy();
copy.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
return new MappingJackson2HttpMessageConverter(copy);
}
}
index 페이지
package com.jjamong.xss.controller;
import java.util.HashMap;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import com.jjamong.xss.data.Search;
@Controller
public class IndexController {
@RequestMapping("/")
public ResponseEntity index(@ModelAttribute Search search) {
HashMap<String, Object> data = new HashMap<String, Object>();
data.put("test1", "안녕");
data.put("test2", "<div>안녕</div>");
data.put("test3", "<script>alert()</script>");
data.put("test4", search.getTest());
return new ResponseEntity(data, HttpStatus.OK);
}
}
실행
{"test4":"<div>alert()</div>","test2":"<div>안녕</div>","test3":"<script>alert()</script>","test1":"안녕"}