본문 바로가기
Spring/Spring Security

Spring Security CSRF / filter / muiltipart-form

by 아이티.파머 2014. 11. 6.
반응형

일단 사용하기에 앞서 Spring Security 사용방법과 CSRF 에 대해 알아야 할것이다.

 

이곳 링크를 통해 확인해 보기 바란다.

링크...

 

Spring Security CSRF  적용을 위해서는 사용중인 스프링과 시큐리티 버전을 확인해보기 바란다.

스프링 시큐리티는 3.2.0 버전 부터 사용 할 수 있고, 지금 여기서는 JAVA Configration 이 아닌

XML 설정과 이후 처리 방법에 대해 알아 보겠다.

 

Security-config.xml 

<http auto-config="false" use-expressions="true" disable-url-rewriting="true" >

     

     <csrf />

     

     <intercept-url pattern="/auth/**"   access="permitAll" />

     <intercept-url pattern="/login/**"   access="permitAll" />

     <intercept-url pattern="/assets/**"  access="permitAll" />

     <intercept-url pattern="/errorpages/**"  access="permitAll" />

     <intercept-url pattern="/guide/**"  access="permitAll" />

     

     <intercept-url pattern="/**" access="hasAnyRole('SOFOS')"  />

     

  <form-login login-page="/auth/auth_form.do"

  login-processing-url="/login"

  default-target-url="/home/homepage.do"

  always-use-default-target="true"

  authentication-failure-handler-ref="AuthLoginFailureHandler"

  authentication-success-handler-ref="AuthLoginSuccessHandler"

  password-parameter="authCode" 

  username-parameter="userCode" />

  

 <logout invalidate-session="true" logout-success-url="/auth/auth_form.do" logout-url="/auth/logout.do" />



 <!--

 ////////////////////////////////////////////////////

 // SESSION MANAGER 

 ////////////////////////////////////////////////////

 -->

 <session-management >

      <concurrency-control max-sessions="1" expired-url="/auth/auth_form.do?resultMessage=DUPLICATION_LOGIN" />

 </session-management>

 <access-denied-handler ref="AccessDeniedHandler"/>

    </http>



 <authentication-manager>

 <authentication-provider ref="AuthenticationProvider" />

 </authentication-manager>



 <beans:bean id="AccessDeniedHandler" class="com.skan.potal.web.auth.handler.AccessDeniedHandler"/>

 <beans:bean id="AuthenticationProvider" class="com.skan.potal.web.auth.provider.AuthenticationProvider"/>

 <beans:bean id="AuthLoginFailureHandler" class="com.skan.potal.web.auth.handler.AuthLoginFailureHandler">

</beans:bean>

 

위와 같이 사용중인 곳에 <csrf />  를 적어 선언함으로 사용 가능 하다.

자이제 Tag를 넣었으니 잘 동작하겠지? 하지만 현실은 그렇치 않다.

 

사용중인 form 전송마다 다음과 같은 테크를 넣어 줌으로 csrf Token 인증을 수행한다.

 

<input type="hidden"   name="${_csrf.parameterName}" value="${_csrf.token}"/>

 

(Spring <form:form > Tag 를 사용하면 자동으로 값을 넣어 주기때문에 폼테그 사용시 위 내용을 선언하지 않아도 사용 가능 하다.)

 

${csrf_ ....} 을 선언하지 않으면 403 접근 권한 오류가 나며 시큐리티에서 사용중인 forbidden Page로 이동 되게 된다.

 

하지만 우리는 모든 페이지 마다 이런 요청을 모두 넣어줄수 없으며, 이 요청을 처리하기 위해 위 테그를 삽입 한다 하여도,  redirect 요청 혹은 forward  요청시 값이 누락될수 있다.

 

이 대안으로 사용 할수 있는 방법은 filter 이다.

모든 요청에 대한 html 값을 읽어  form 테그를 찾아  마지막에 끝나는 지점에 대해.

위 테그를 자동(강제) 삽입하는 것이다.

 

 

OOOTokenFilter.java

 

import java.io.IOException;



import javax.servlet.Filter;

import javax.servlet.FilterChain;

import javax.servlet.FilterConfig;

import javax.servlet.ServletException;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;



import org.apache.commons.lang.StringUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.context.SecurityContext;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.web.csrf.CsrfToken;

import org.springframework.security.web.csrf.CsrfTokenRepository;

import org.springframework.stereotype.Component;



@Component

public class FormTokenVerificationFilter implements Filter

{



 ////////////////////////////////////////////////////////////////////////////////

 //

 ////////////////////////////////////////////////////////////////////////////////

 private static final String[] EXCLUDE_URL_LIST = { "/logout", "/assets", "/errorpages", "/WEB-INF", "/report/download" };



 ////////////////////////////////////////////////////////////////////////////////

 //

 ////////////////////////////////////////////////////////////////////////////////

 private static final String PREFIX_NAME = "_";

 private static final String SESSION_TOKEN_NAME = "SESSION_TOKEN_NAME";

 private static final String SESSION_ACTIVE_TOKEN = "SESSION_ACTIVE_TOKEN";



 private static final String REDIRECT_URL = "/potal/auth/auth_form.do?resultMessage=INVALID_TOKEN";





 ////////////////////////////////////////////////////////////////////////////////

 //

 ////////////////////////////////////////////////////////////////////////////////

 private Logger logger = LoggerFactory.getLogger( this.getClass() );



 @Autowired

 private AuditLogDao AuditDao;



 @Autowired

 private CsrfTokenRepository csrfTokenRepository;



 @Override

 public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException{





 if( request instanceof HttpServletRequest ){





 HttpServletRequest httpRequest = (HttpServletRequest) request;

 HttpServletResponse httpResponse = (HttpServletResponse) response;





 HttpSession httpSession = httpRequest.getSession();





 boolean excludeState = false;

 String reqUrl = httpRequest.getRequestURL().toString();

 for( String target : EXCLUDE_URL_LIST )

 {

 if( reqUrl.indexOf( target ) > -1 )

 {

 excludeState = true;

 break;

 }

 }



 if( excludeState )

 {

 chain.doFilter( request, response );

 return;

 }



 ServletResponse newResponse = new FormTokenResponseWrapper( httpResponse );



 chain.doFilter( request, newResponse );





 if( newResponse instanceof FormTokenResponseWrapper ) {





 String tokenName = PREFIX_NAME + RandomCodeUtils.generate();

 String tokenValue = RandomCodeUtils.generate();

 String encryptedTokenValue = CryptoUtils.encrypt( tokenValue );





 CsrfToken token = (CsrfToken) request.getAttribute("_csrf");

 // Spring Security will allow the Token to be included in this header name

 //response. setHeader("X-CSRF-HEADER", token.getHeaderName());

 // Spring Security will allow the token to be included in this parameter name

 //response.setHeader("X-CSRF-PARAM", token.getParameterName());

 // this is the value of the token to be included as either a header or an HTTP parameter

 //response.setHeader("X-CSRF-TOKEN", token.getToken());



 //final CsrfToken token = csrfTokenRepository.loadToken(httpRequest);

 String tokenStr = "";

 if(token != null) {

 tokenStr = String.format("<input type=\"hidden\" name=\"%s\" value=\"%s\" />", token.getParameterName(), token.getToken());

 }

 //////////////////////////////////////////////////////////////////////////

 //

 //////////////////////////////////////////////////////////////////////////

 String responseText = newResponse.toString();

 if( responseText != null && httpSession != null )

 {



 httpSession.removeAttribute( SESSION_ACTIVE_TOKEN );

 httpSession.removeAttribute( SESSION_TOKEN_NAME );



 //////////////////////////////////////////////////////////////////////////

 //

 //////////////////////////////////////////////////////////////////////////

 httpSession.setAttribute( SESSION_TOKEN_NAME, tokenName );

 httpSession.setAttribute( SESSION_ACTIVE_TOKEN, tokenValue );



 logger.debug( "===============================");

 logger.debug( "TOKEN-NAME : {} ", httpRequest.getRequestURI() );

 logger.debug( "TOKEN-NAME : {} ", tokenName );

 logger.debug( "TOKEN-VALUE : {} ", tokenValue );



 String formTokenInput = String.format("<input type=\"hidden\" name=\"%s\" value=\"%s\" />"

 + tokenStr +"</form>", tokenName, encryptedTokenValue );

 responseText = StringUtils.replace( responseText, "</form>", formTokenInput );



 response.getWriter().write( responseText );

 }

 }



 }

 else {

 chain.doFilter( request, response );

 }



 }



 @Override

 public void init( FilterConfig arg0 ) throws ServletException {

 }



 @Override

 public void destroy() {

 }



}

 

 

formTokenInput 변수에 사용한것 처럼 모든 응답요청에 대하여 CSRF Tag 를 삽입 하도록 한다.

CSRF  테그 삽입시 CsrfToken token = (CsrfToken) request.getAttribute("_csrf"); 

형태로 값을 가져 올 수 있다.

 

CSRF  토큰이 전달되는 모습을 fiddler 등  http 요청을 감시하는 툴을 통해 확인 할 수 있다.

아큐넥티스로 확인해도 취약점이 안나오는것을 확인 할 수 있다.

 

 

하지만,  muiltipart-form 의경우 403 페이지로 권한이 없다고 페이지가 이동되게 되는데

muiltipart-form 의 경우는

<form Tag 에 다음과 같이 추가하여 준다.

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

 

 

마지막으로 잘못된 토큰에 의해 오류 발생시 스프링에서 제공 중인 forbidden  페이지를 사용하지 않고, 싶다면

AccessDeniedHandler 이녀석을 상속 받아 다음과 같이 구현하도록 한다.

 

 

import java.io.IOException;



import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



import org.springframework.security.access.AccessDeniedException;

import org.springframework.security.web.access.AccessDeniedHandler;



public class AccessDeniedHandler implements AccessDeniedHandler {



 @Override

 public void handle(HttpServletRequest request,

 HttpServletResponse response,

 AccessDeniedException accessDeniedException) throws IOException,

 ServletException {



 response.sendRedirect("errorPageURL"); 

 }

}

 

Forbidden 시에 원하는 페이지로 이동 되는것을 확인 할 수 있다.

 

끝~~

 

http://info.michael-simons.eu/2014/01/29/csrf-protection-with-spring-security-revisited/

http://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html#csrf-include-csrf-token

반응형