[Spring Framework]Lucy-xss-filter-servlet 적용하기
웹 개발을 할 때 보안은 중요한 요소입니다.
오늘 포스팅할 내용은 웹 취약점 공격 방법의 일종 중 하나인 XSS를 방어하는 방법에 관한 것입니다.
테스트 환경
- JDK 1.8
- Spring 4.3.14
- Tomcat 9.0
XSS(Cross Site Scripting)
XSS는 웹 어플리케이션에 악의적으로 스크립트를 삽입해 공격하는 기법을 말한다.
만약 웹 어플리케이션에서 데이터를 서버로 저장할 때(게시판에 글을 쓴다던지. 회원 정보를 수정한다던지) 데이터를 검증하지 않거나 XSS에 대한 방어 대비가 없다면 스크립트가 포함된 데이터가 저장되어 유저로 하여금 원치 않는 스크립트를 실행시킬 수 있다.
이러한 방식의 위험성은 일반적으로 자바스크립트에서 발생하지만 VB 스크립트, Active X등과 같은 동적 데이터를 생성하는 모든 언어에서 발생할 수 있다. 이는 유저의 정보를 탈취하거나, 조직 내부의 데이터가 탈취되는 등 매우 위험한 상황으로 이어질 수 있다.
이에 대비해 네이버에서 개발한 XSS 필터링 기법인 Lucy-xss-servlet-filter를 소개한다.
Lucy-xss-servlet-filter
Lucy-xss-servlet-filter는 웹어플리케이션으로 들어오는 모든 요청 파라미터에 대해 기본적인 XSS 방어 필터링을 수행한다. 자세한 내용과 적용 방법은 아래 사이트에 친절히 소개되어 있다.
그럼 Lucy-xss-servlet-filter를 적용시켜보자.
1) 먼저 메이븐을 통해 라이브러리를 등록한다.
pom.xml
<!-- Lucy-xss-servlet-filter -->
<dependency>
<groupId>com.navercorp.lucy</groupId>
<artifactId>lucy-xss-servlet</artifactId>
<version>2.0.1</version>
</dependency>
2) web.xml에 filter를 추가한다.
CharacterEncodingFilter가 있다면 XssEscapeServletFilter가 뒤에 위치하도록 해준다.
web.xml
<filter>
<filter-name>xssEscapeServletFilter</filter-name>
<filter-class>com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>xssEscapeServletFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3) src/main/resources 경로에 lucy-xss-servlet-filter-rule.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-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에서 들어오는 'gParam' 파라미터는 필터링 되지 않으며 또한 'g'로 시작하는 파라미터도 필터링 되지 않는다. -->
<params>
<param name="gParam" useDefender="false" />
<param name="g" usePrefix="true" useDefender="false" />
</params>
</global>
<!-- URL 별 필터링 룰 선언 -->
<url-rule-set>
<!-- url disable이 true이면 지정한 url 내의 모든 파라미터는 필터링되지 않는다.(기본값은 false) -->
<url-rule>
<url disable="true">/xss/noneFilter.do</url>
</url-rule>
<!--
1. '/xss/globalFilter.do' 내의 nParam은 필터링 되지 않는다.
2. '/xss/globalFilter.do' 내의 'n'으로 시작하는 파라미터는 필터링 되지 않는다.(usePrefix=true)
-->
<url-rule>
<url>/xss/globalFilter.do</url>
<params>
<param name="nParam" useDefender="false" />
<param name="n" usePrefix="true" useDefender="false" />
</params>
</url-rule>
</url-rule-set>
</config>
기본 설정을 그대로 가져왔고 테스트 해볼 부분만 수정을 했다.
3~29번 라인에 등록된 defender는 Lucy-xss-servlet-filter에서 사용되는 필터들이다. xssPreventerDefender, xssSaxFilterDefender, xssFilterDefender로 구분된다.
xssPreventerDefender는 모든 태그를 치환해서 변환해주며 xssFilterDefender, xssSaxFilterDefender는 화이트 리스트(특정 태그에 대해서만 필터)를 통해 예외처리가 가능한 필터이다.
여기까기 기본 설정을 끝내고 세가지 상황을 테스트했다.
- Lucy 필터 적용
- Lucy 필터 규칙사용
- JSON 데이터 테스트
1. Lucy 필터 적용
Post 방식으로 데이터를 전달하는 기본 테스트 페이지를 만들었다.
ex01.jsp에서 스크립트 작성된 문자열을 서버로 전송하도록 해봤다. ON 버튼은 필터를 사용하도록 하고 OFF 버튼은 필터 사용하지 않도록 했다.(lucy-xss-servlet-filter-rule.xml 48~50라인 참고)
ex01.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>XSS Filter Example</title>
<%@ include file="/WEB-INF/views/include/source.jsp" %>
<script>
$(document).ready(function() {
$("#btnOn").click(function() {
$("#testForm").prop("action", "/xss/useFilter.do");
var url = $("#testForm").attr("action");
var data = $("#testForm").serialize();
$.post(url, data).done();
});
$("#btnOff").click(function() {
$("#testForm").prop("action", "/xss/noneFilter.do");
var url = $("#testForm").attr("action");
var data = $("#testForm").serialize();
$.post(url, data).done();
});
});
</script>
</head>
<body>
<form id="testForm" method="post">
<div class="container">
<div class="row border-bottom mb-3">
<div class="col-12">
<p class="h1">EX01. Lucy-xss-servlet-filter 적용</p>
</div>
</div>
<div class="row">
<div class="col-5">
<div class="row">
<div class="col-2">
<label for="inputLabel" class="visually-hidden"></label>
<input type="text" readonly class="form-control-plaintext" id="inputLabel" value="메세지">
</div>
<div class="col-auto">
<label for="inputMsg" class="visually-hidden"></label>
<input type="text" class="form-control" id="inputMsg" name="inputMsg">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3" id="btnOn">ON</button>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3" id="btnOff">OFF</button>
</div>
</div>
</div>
</div>
</div>
<%@ include file="/WEB-INF/views/common/bottom.jsp" %>
</form>
</body>
</html>
XssController.java
@RequestMapping(value = "/xss/useFilter.do", method = { RequestMethod.POST })
public ModelAndView useFilter(HttpServletRequest request) throws Exception {
ModelAndView mav = new ModelAndView();
String inputMsg = request.getParameter("inputMsg");
String convertMsg = XssPreventer.unescape(inputMsg);
logger.info("### Get Message(Use XSS Filter) ###");
logger.info("### 치환 => " + inputMsg);
logger.info("### 역치환 => " + convertMsg);
mav.addObject("msg", inputMsg);
mav.setViewName("/common/message");
return mav;
}
@RequestMapping(value = "/xss/noneFilter.do", method = { RequestMethod.POST })
public ModelAndView noneFilter(HttpServletRequest request) throws Exception {
ModelAndView mav = new ModelAndView();
String inputMsg = request.getParameter("inputMsg");
logger.info("### Get Message(None XSS Filter) ###");
logger.info("### => " + inputMsg);
mav.addObject("msg", inputMsg);
mav.setViewName("/common/message");
return mav;
}
로그를 찍어보면 넘어온 문자가 치환된 것을 확인할 수 있다.
XssPreventer.unescape()를 사용하면 역치환도 가능하다.
참고로 필터링이 되는 시점은 getParameter로 값을 가져오는 시점이다.
치환된 문자는 message 페이지로 보내 el/jstl 태그를 이용해 출력했다.
message.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
메세지(EL) : ${msg }
<br>
메세지(JSTL) : <c:out value="${msg}" />
[결과]
el 태그로 사용한 경우 스크립트 문자가 그대로 출력된다.(스크립트가 실행은 되지 않았지만) 반면 jstl 태그를 사용한 경우 치환된 문자열이 출력된다.
이번에는 OFF 버튼을 클릭해 필터를 사용하지 않았다.
로그를 보면 치환되지 않은 것을 확인할 수 있다.
message 페이지로 이동하자 스크립트가 작동했다. 아마도 el 태그에서 스크립트가 작동됐을 것이다. jstl 태그는 문자열로 적혀있을 뿐 스크립트가 실행되지 않는다. 이것은 jstl의 escapeXml 속성 때문이다. escapeXml은 HTML 태그를 text로 적용시킨다. 그래서 el 태그를 쓸 때는 XSS 취약점을 조심해야 한다.
2. Lucy 필터 규칙사용
특정 url과 파라미터에 대해 필터링 옵션을 주고 싶다면 <url-rule-set /> 태그를 사용하면 된다.(lucy-xss-servlet-filter-rule.xml 45번 라인 참고)
테스트 페이지를 하나 만들었고 여기서는 데이터 전송시 /xss/globalFilter.do를 호출한다.
필터링 규칙은 다음과 같이 선언했다.
- /xss/globalFilter.do 내의 nParam의 이름을 갖는 파라미터는 필터링되지 않는다.
- /xss/globalFilter.do 내의 n으로 시작하는 파라미터는 필터링되지 않는다.
ex02.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>XSS Filter Example</title>
<%@ include file="/WEB-INF/views/include/source.jsp" %>
<script>
$(document).ready(function() {
$("#btnOn").click(function() {
var url = $("#testForm").attr("action");
var data = $("#testForm").serialize();
$.post(url, data).done();
});
});
</script>
</head>
<body>
<form id="testForm" action="/xss/globalFilter.do" method="post">
<div class="container">
<div class="row border-bottom mb-3">
<div class="col-12">
<p class="h1">EX02. Lucy-xss-servlet-filter 필터 규칙사용</p>
</div>
</div>
<div class="row mb-1">
<div class="col-1">
<label for="inputLabel1" class="visually-hidden"></label>
<input type="text" readonly class="form-control-plaintext" id="inputLabel1" value="필터 미적용">
</div>
<div class="col-auto">
<label for="inputMsg" class="visually-hidden"></label>
<input type="text" class="form-control" id="nParam" name="nParam">
</div>
</div>
<div class="row mb-1">
<div class="col-1">
<label for="inputLabel2" class="visually-hidden"></label>
<input type="text" readonly class="form-control-plaintext" id="inputLabel2" value="필터 미적용">
</div>
<div class="col-auto">
<label for="inputMsg" class="visually-hidden"></label>
<input type="text" class="form-control" id="nMsg" name="nMsg">
</div>
</div>
<div class="row mb-1">
<div class="col-1">
<label for="inputLabel3" class="visually-hidden"></label>
<input type="text" readonly class="form-control-plaintext" id="inputLabel3" value="필터 적용">
</div>
<div class="col-auto">
<label for="inputMsg" class="visually-hidden"></label>
<input type="text" class="form-control" id="msg" name="msg">
</div>
</div>
<div class="row">
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3" id="btnOn">SEND</button>
</div>
</div>
</div>
<%@ include file="/WEB-INF/views/common/bottom.jsp" %>
</form>
</body>
</html>
XssController.java
@RequestMapping(value = "/xss/globalFilter.do", method = { RequestMethod.POST })
public ModelAndView globalFilter(HttpServletRequest request) throws Exception {
ModelAndView mav = new ModelAndView();
String nParam = request.getParameter("nParam");
String nMsg = request.getParameter("nMsg");
String msg = request.getParameter("msg");
logger.info("### Get Message(<global> 태그 사용) ###");
logger.info("### nParam => " + nParam);
logger.info("### nMsg => " + nMsg);
logger.info("### msg => " + msg);
mav.addObject("nParam", nParam);
mav.addObject("nMsg" , nMsg);
mav.addObject("msg" , msg);
mav.setViewName("/xss/ex02_1");
return mav;
}
로그를 보면 nParam, n으로 시작하는 nMsg 파라미터는 필터링되지 않았으며 msg 파라미터만 필러링된 것을 확인할 수 있다.
[결과]
3. JSON 데이터는?
Lucy 필터는 아쉽게도 form-data에 대해서만 적용되고 Request Raw Body로 넘어가는 JSON 데이터에 대해서는 처리되지 않는다.
테스트 페이지에서는 ajax로 데이터를 전송하고 json 형태로 데이터를 받는다.
ex03.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>XSS Filter Example</title>
<%@ include file="/WEB-INF/views/include/source.jsp" %>
<script>
function sendMsg() {
var params = {
msg : $("#inputMsg").val()
};
$.ajax({
type : "POST",
async : false,
url : "/xss/jsonFilter.do",
cache : false,
dataType : "json",
data : JSON.stringify(params),
contentType: "application/json; charset=utf-8",
success: function(data) {
var msg = data.msg;
$("#result").html(msg);
},
error : function(request, status, error) {
alert("code : " + request.status + "\n" + "message : " + request.responseText + "\n" + "error : " + error);
}
});
}
$(document).ready(function() {
});
</script>
</head>
<body>
<form id="testForm" method="post">
<div class="container">
<div class="row border-bottom mb-3">
<div class="col-12">
<p class="h1">EX03. Lucy-xss-servlet-filter JSON 데이터는?</p>
</div>
</div>
<div class="row mb-3">
<div class="col-2">
<label for="inputLabel" class="visually-hidden"></label>
<input type="text" readonly class="form-control-plaintext" id="inputLabel" value="메세지">
</div>
<div class="col-auto">
<label for="inputMsg" class="visually-hidden"></label>
<input type="text" class="form-control" id="inputMsg" name="inputMsg">
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary mb-3" onclick="sendMsg()">SEND</button>
</div>
</div>
<div class="row">
<div class="col-auto">
결과 : <span id="result"></span>
</div>
</div>
</div>
<%@ include file="/WEB-INF/views/common/bottom.jsp" %>
</form>
</body>
</html>
XssController.java
@RequestMapping(value = "/xss/jsonFilter.do", method = { RequestMethod.POST })
public @ResponseBody JSONObject sqlExceptionPost(@RequestBody String jsonParam) throws Exception {
JSONObject result = new JSONObject();
JSONObject json = (JSONObject) JSONValue.parse(jsonParam);
String msg = (String) json.get("msg");
logger.info("### Get Message(JSON 데이터) ###");
logger.info("### => " + msg);
result.put("msg", msg);
return result;
}
로그를 확인해보면 예상한대로 필터링되지 않는다.
[결과]
따라서 @RequestBody로 전달되는 JSON 데이터의 경우 별도의 XSS 방어 구현이 필요하다.
참고자료
전체 소스는 GitHub에서 확인하실 수 있습니다.
+피드백은 언제나 환영입니다 :)