single-page application (SPA) with Spring Security’s CSRF protection

wanuki·2023년 12월 20일

REST API 서버를 개발하면서 학습한 SPA 애플리케이션과의 통신에서 세션 대신 쿠키를 이용해 CSRF을 방지하는 spring security 기능을 알아보겠습니다.

CSRF 보호

  • spirng security에서 CSRF을 방지하기 위해 기본적으로 세션에 토큰을 저장하고 HTTP 요청 헤더 또는 파라미터를 통해 CSRF 토큰을 확인합니다.

  • Thymeleaf와 같은 뷰 템플릿은 POST와 같은 안전하지 않은 HTTP Method일 경우 자동으로 CSRF 토큰을 포합시킵니다.

HTML Form 요청에 포함된 CSRF 토큰 hidden 태그 예시

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"	method="post">
	<input type="submit" value="Log out" />
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>

SPA에서 CSRF 보호

  • 일반적으로 single-page application는 HTML 대신 JSON을 사용합니다.
    따라서 세션 대신 쿠키에 CSRF토큰을 저장하는 기능을 사용합니다.

  • Angular와 같은 자바스크립트 프레임워크는 자동으로 쿠키의 CSRF 토큰을 HTTP 요청 헤더로 포함하는 기능을 제공합니다.

Spring Security 쿠키 CSRF 보호 설정

Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.csrf((csrf) -> csrf
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   
				.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())            
			)
			.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); 
		return http.build();
	}
}
  • 쿠키를 자바스크립트 응용 프로그램에서 읽을 수 있도록 HttpOnly를 false로 설정한 CookieCsrfTokenRepository를 구성합니다.
  • 요청에 따라 CSRF 토큰을 해결하는 SpaCsrfTokenRequestHandler 구성합니다.
  • 쿠키를 로드하도록 CsrfCookieFilter를 구성합니다.

SpaCsrfTokenRequestHandler

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
	private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
		this.delegate.handle(request, response, csrfToken);
	}

	@Override
	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
		if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
			return super.resolveCsrfTokenValue(request, csrfToken);
		}
		return this.delegate.resolveCsrfTokenValue(request, csrfToken);
	}
}
  • CsrfToken의 BREACT 보호를 위해 XorCsrfTokenRequestAttributeHandler 클래스를 이용한다고 함 (아직 이해가 덜 된 부분이라 추가 학습이 더 필요함)
  • resolveCsrfTokenValue 메서드에서 CSRF 토큰이 헤더에 있는지 파라미터에 있는지에 따라 다르게 처리합니다.

CsrfCookieFilter

final class CsrfCookieFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
		csrfToken.getToken();
		filterChain.doFilter(request, response);
	}
}
  • CsrfFilter에서 발급한 지연 CSRF 토큰을 csrfToken.getToken() 호출을 통해 토큰 값을 쿠키로 렌더링합니다.

CSRF 테스트

  • GET 요청을 통해 쿠키에 저장된 CSRF 토큰을 확인 할 수 있습니다.

  • POST 요청 헤더에 CSRF 토큰을 추가하면 정상적으로 응답을 받을 수 있습니다.

인증 또는 로그아웃 성공

  • 인증 또는 로그아웃 성공 시 CsrfAuthenticationStrategy 및 CsrfLogoutHandler에서 이전 토큰을 삭제하므로 필요하다면 클라이언트 애플리케이션은 새로 CSRF 토큰을 발급해야 합니다.
  • 쉽게 말해서 인증 또는 로그아웃을 성공하면 바로 POST 요청을 할 수 없고 GET 요청으로 새로 CSRF 토큰을 발급받은 후 POST 요청을 해야 합니다.

https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html

profile
하늘은 스스로 돕는 자를 돕는다

0개의 댓글