관리 메뉴

한글창제의 기쁨

Spring Security CSRF / filter / muiltipart-form 본문

Spring/Spring Security

Spring Security CSRF / filter / muiltipart-form

timesurfer 공간지배자 2014.11.06 16:38

일단 사용하기에 앞서 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 테그를 찾아  마지막에 끝나는 지점에 대해.

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


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() {

}


}



빨간 부분으로 처리 한것 처럼 모든 응답요청에 대하여 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

저작자 표시
신고
4 Comments
  • 스프링입문자 2016.11.05 14:50 신고 안녕하세요. ^^
    스프링을 사용한지 3개월되는 초보 입문자입니다.
    로그인 기능을 하는 api를 만들려는 중인데 막히는 부분이 있어서 구글링을 하다가 여기까지 왔습니다. ;;

    현재 제가 spring boot v1.3.3 base로 코딩을 하고 있는데,(오픈소스를 받아서 그 틀 안에서 개발을 진행중입니다)
    클라이언트 단에서 REST 호출을 하면 "Expected CSRF token not found. Has your session expired?"란 에러가 발생합니다.
    위와 같이 서버단, 클라이언트 단에서 security 처리를 해줘야 할듯한데..
    3버전 이상부터 가능하다고 하니, 제가 사용하는 버전에서는 해결방법이 있는지 알고싶습니다..
    감사합니다.

  • Favicon of http://mycup.tistory.com BlogIcon timesurfer 공간지배자 2017.05.30 14:59 신고 스플이 부트 1.3.3 버전을 사용하시면서 시큐리티 버전은 몇버전을 사용중이신가요? 스프링 부트의 기본 시큐리티를 쓰신담면 3.0 이상이 실테니 문제 없으실 거에요. 시간이 지나서 해결하셨 겠지만, 해당 오류는 csrf 설정이 되었는데 토큰이 값이 없기때문에 접근할수 없다고 뜨는 에러입니다. 각각의 form 테그에 csrf token이 생성되도록 구현해주세요 혹은 spring <form:form 테그를 이용하시길 권고합니다
  • 백진영 2017.02.21 11:26 신고 닷넷 개발자입니다 ~ ^^ csrf 스프링에선어떻게 구현할까 호기심이 생겨 검색하다 들어왔습니다. 필터링으로 주입하는 방법이 있는줄을 상상을 못했네요 좋은글 잘읽고갑니다
  • Favicon of http://mycup.tistory.com BlogIcon timesurfer 공간지배자 2017.05.30 14:55 신고 재미있게 읽어 주셔서 감사합니다 ^^
댓글쓰기 폼