본문 바로가기

웹 보안

[XSS, CSRF] Spring에 적용

스프링 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("&lt;");
                        break;
                    case '>':
                        strBuff.append("&gt;");
                        break;
                    case '&':
                        strBuff.append("&amp;");
                        break;
                    case '"':
                        strBuff.append("&quot;");
                        break;
                    case '\'':
                        strBuff.append("&apos;");
                        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>

적용 결과

board_lucy적용2

guestbook_lucy 적용

board_lucy적용

Reference

lucy-xss-servlet-filter

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;
        }

1

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>

Reference

Spring security 자세한 설명