스프링 XSS, CSRF 적용
XSS, CSRF 적용
XSS
XSS 과거 코드
과거 코드들을 보면 HttpServletRequestWrapper 를 상속해서 아래와 같이 처리하는 것을 종종 볼 수 있습니다.
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
StringBuffer strBuff = new StringBuffer();
for (int j = 0; j < values[i].length(); j++) {
char c = values[i].charAt(j);
switch (c) {
case '<':
strBuff.append("<");
break;
case '>':
strBuff.append(">");
break;
case '&':
strBuff.append("&");
break;
case '"':
strBuff.append(""");
break;
case '\'':
strBuff.append("'");
break;
default:
strBuff.append(c);
break;
}
}
values[i] = strBuff.toString();
} else {
values[i] = null;
}
}
return values;
}
네이버에서 개발한 lucy-xss-servlet-filter
<!-- pom.xml -->
<dependency>
<groupId>com.navercorp.lucy</groupId>
<artifactId>lucy-xss-servlet</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Web.xml -->
<!-- XSS filter -->
<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>
<!-- /resources 폴더 아래 lucy-xss-servlet-filter-rule.xml 파일 생성 -->
<!-- 옵션 설명 https://github.com/naver/lucy-xss-servlet-filter/blob/master/doc/manual.md -->
<?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에서 들어오는 globalParameter 파라메터는 필터링 되지 않으며
또한 globalPrefixParameter로 시작하는 파라메터도 필터링 되지 않는다. -->
<params>
<param name="globalParameter" useDefender="false" />
<param name="globalPrefixParameter" usePrefix="true" useDefender="false" />
</params>
</global>
<!-- url 별 필터링 룰 선언 -->
<url-rule-set>
<!-- url disable이 true이면 지정한 url 내의 모든 파라메터는 필터링 되지 않는다. -->
<url-rule>
<url disable="true">/disableUrl1.do</url>
</url-rule>
<!-- url1 내의 url1Parameter는 필터링 되지 않으며 또한 url1PrefixParameter로 시작하는 파라메터도 필터링 되지 않는다. -->
<url-rule>
<url>/url1.do</url>
<params>
<param name="url1Parameter" useDefender="false" />
<param name="url1PrefixParameter" usePrefix="true" useDefender="false" />
</params>
</url-rule>
<!-- url2 내의 url2Parameter1만 필터링 되지 않으며 url2Parameter2는 xssSaxFilterDefender를 사용해 필터링 한다. -->
<url-rule>
<url>/url2.do</url>
<params>
<param name="url2Parameter1" useDefender="false" />
<param name="url2Parameter2">
<defender>xssSaxFilterDefender</defender>
</param>
</params>
</url-rule>
</url-rule-set>
</config>
적용 결과
Reference
CSRF
Referrer 검증
<!-- spring-servlet.xml -->
<!-- Interceptors -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/auth" />
<bean class="com.cafe24.security.AuthLoginInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/user/logout" />
<bean class="com.cafe24.security.AuthLogoutInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/*/admin/**" />
<bean class="com.cafe24.security.AuthAdminInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/user/auth" />
<mvc:exclude-mapping path="/user/logout" />
<mvc:exclude-mapping path="/assets/**" />
<mvc:exclude-mapping path="/*/admin/**" />
<!-- AuthInterceptor로 모든 접속 경로 확인하므로 체크-->
<bean class="com.cafe24.security.AuthInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// Referrer 체크(csrf 방어를 위하여)
String prev_url =(String)request.getHeader("REFERER");
System.out.println("이 전 페이지:"+prev_url);
if (prev_url == null ) {
prev_url = "fail";
}
if (prev_url.matches("^*/jblog2/*")) {
System.out.println("접속 승인");
return true;
}
else if(prev_url.equals(("fail"))) {
System.out.println("접속차단");
return false;
}
Reference CSRF 방어 방법
Session에 CSRF Token 생성 및 검증
모든 접근은 AuthInterceptor로 들어온다. 아래의 코드에 csrf 토큰 생성을 추가하여 접근 시 배정받을 수 있게한다.
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//CSRF 방어를 위한 TOKEN 생성
HttpSession tokenSession = request.getSession();
String token = UUID.randomUUID().toString();
tokenSession.setAttribute("CSRF_TOKEN", token);
System.out.println("토큰생성"+ token);
로그인 시 hidden 값으로 CSRF_TOKEN을 셋팅하여 보낸다.
즉, 어디서든 접속하면 TOKEN이 생성되며 생성된 TOKEN은 아래의 값에 셋팅되어 CONTROLLER(여기서는 Interceptor) 로 넘어간다.
<form:form modelAttribute="userVo" class="login-form" id="login-form"
method="post"
action="${pageContext.request.contextPath}/user/auth">
<input type="hidden" name="_csrf" value="${CSRF_TOKEN}" />
<label>아이디</label> <input type="text" name="id">
<label>패스워드</label> <input type="text" name="password">
<input type="submit" value="로그인">
</form:form>
JSP form을 통해서 넘어온 token 데이터가 세션에 잡혀있는 token 데이터와 동일한 지 확인한다.
만일 동일 사용자가 동일 페이지에서 넘겼다면 정상작동할 것이며 아닌 경우에는 false로 정보를 패스하지 않는다.
public class AuthLoginInterceptor extends HandlerInterceptorAdapter {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String id = request.getParameter("id");
String password = request.getParameter("password");
// 파라미터로 전달된 csrf 토큰 값
String param = request.getParameter("_csrf");
System.out.println("파라미터러 넘어온녀석 "+param);
// 세션에 저장된 토큰 값과 일치 여부 검증
if(param == null) {
System.out.println("param 없음 ");
return false;
}
if (request.getSession().getAttribute("CSRF_TOKEN").equals(param)){
System.out.println("동일 token");
} else if(!request.getSession().getAttribute("CSRF_TOKEN").equals(param)) {
System.out.println("다른 토큰입니다. "+request.getSession().getAttribute("CSRF_TOKEN") );
System.out.println("시스템 생성 토큰 : "+ param);
return false;
}
Spring security
pom.xml
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.2.3.RELEASE</version>
</dependency>
web.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
classpath:security-context.xml <--- 추가
</param-value>
</context-param>
<!-- Security filter 추가 -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
security-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<http auto-config='true' use-expressions="true">
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/resources/**" access="permitAll" />
<intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
<!--
로긴 시 url
성공 시 url
가져올 정보, id pw
실패 시 url
always use default 설정하지 않을 시 default taget url로 제대로 가지 않음
-->
<form-login login-page="/user/login"
default-target-url="/main"
username-parameter="username"
password-parameter="password"
authentication-failure-url="/error"
always-use-default-target='true'/>
<!--
로그아웃 시도 시 url
성공 시 이동 url
-->
<logout invalidate-session="true" logout-url="/user/logout"
logout-success-url="/main" />
<!-- enable csrf protection -->
<csrf />
</http>
<authentication-manager>
<authentication-provider user-service-ref="userService" />
</authentication-manager>
<beans:bean id="userService" class="com.cafe24.jblog.service.userService">
</beans:bean>
</beans:beans>