본문 바로가기
☕Java/Spring

스프링 시큐리티로 CSRF 방지 설정하기

by 캔 2024. 3. 17.

CSRF 공격이란?

CSRF(Cross Site Request Forgery) 공격은 사용자가 의도하지 않은 요청을 악용하여 악의적인 행위를 유도하는 기법이다. 웹 애플리케이션은 일반적으로 사용자를 인증하기 위해 세션 쿠키나 인증 토큰을 사용하는데, CSRF 공격자는 이 인증 정보를 이용하여 피해자인 것처럼 요청을 보낸다. 이를 통해 공격자는 피해자의 권한으로 악의적인 동작을 수행할 수 있다. 예를 들어, 피해자가 로그인한 상태에서 공격자가 조작한 링크를 클릭하면, 웹 애플리케이션은 피해자의 인증 정보를 사용하여 공격자가 조작한 요청을 수행한다. 이로써 공격자는 피해자의 계정으로 의도하지 않은 동작을 수행할 수 있게 된다.

 

스프링 시큐리티에서는 기본적으로 CSRF 방지를 활성화한다. HttpSecurity 개체의 CSRF 메서드에 아래와 같이 기본 설정을 명시해 줄 수도 있다. 후술할 설정들도 명시하지 않으면 기본값을 사용하고, 그 기본값을 명시적으로 작성하거나 커스텀 설정으로 변경해서 사용이 가능하다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf(Customizer.withDefaults());
		return http.build();
	}
}

 

특정 엔드포인트나 경로에 대해서만 CSRF 방지를 사용하지 않으려면 다음과 같이 사용한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers("/api/*")
            );
        return http.build();
    }
}

 

권장하지는 않지만, CSRF 방지를 사용하지 않으려면 다음과 같이 설정한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf.disable());
		return http.build();
	}
}

 

 

CsrfFilter 구성요소

CSRF 방지는 CsrfFilter의 다음의 구성요소들을 사용하여 이뤄진다.

 

  • CsrfTokenRepository
  • CsrfTokenRequestHandler
  • AccessDeniedHandler
  • RequestMatcher

 

스프링 시큐리티 CSRF 방지 프로세스

  1. CSRF 필터는 CsrfTokenRepository에 토큰을 요청하고 DeferredCsrfToken을 받아온다.(성능 개선을 위해 지연된 로딩을 사용)
  2. CsrfTokenRequestHandler에 받아온 CsrfToken을 전달하여 애플리케이션의 다른 부분에서 사용가능하도록 요청 속성에 추가한다.
  3. RequestMatcher와 비교하여 CSRF 방지 대상이 아니면 다음 필터로 넘어가고, 대상이면 CSRF 방지 절차를 시작한다.
  4. 이제 지연됐던 DeferredCsrfToken에 CsrfToken이 완전히 로드되고, CsrfTokenRequestHandler를 사용하여 클라이언트가 제공한 실제 CsrfToken를 찾는다.
  5. 실제 CsrfToken이 저장된 CsrfToken와 같으면 절차가 종료된다. 만약 실패하면, AccessDeniedException이 발생하고 AccessDeniedHandler가 이를 처리한다.

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

 

스프링 시큐리티는 기본적으로 세션에 CSRF 토큰을 저장하는 HttpSessionCsrfTokenRepository을 사용한다. 쿠키에 저장하는 CookieCsrfTokenRepository를 사용하거나 커스텀 리포지터리를 구현할 수도 있다. 이 리포지터리들은 요청 헤더 X-CSRF-TOKEN이나 요청 파라미터 _csrf에서 토큰을 가져온다. CookieCsrfTokenRepository  사용 시에는 이 토큰을 쿠키에 XSRF-TOKEN라는 이름으로 저장한다.

 

아래는 CookieCsrfTokenRepository을 사용하며 쿠키의 httpOnly 설정을 false로 설정한다. 이렇게 하면 자바스크립트에서 쿠키에 접근이 가능하기 때문에, Angular, React 등 자바스크립트를 사용하는 프레임워크나 AJAX 요청 시에도 쿠키에서 CSRF를 가져올 수 있다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
			);
		return http.build();
	}
}

 

CSRF 토큰은 CsrfTokenRequestHandler를 사용하여 사용 가능한 상태로 만들어진다. 별도로 설정하지 않을 경우 스프링 시큐리티는 XorCsrfTokenRequestHandler를 사용한다. XorCsrfTokenRequestHandler는 추가적으로 BREACH 공격 방지 기능을 제공한다. 이 기능을 포함하지 않으려면 CsrfTokenRequestHandler를 사용하도록 설정하면 된다. BREACH 방지 기능은 CSRF 토큰을 무작위로 암호화하여 매 요청마다 반환받는 CSRF 토큰을 변경한다. 헤더나 파라미터의 토큰이 있으면 복호화하여 저장된 토큰과 비교하게 된다.

 

이제 서버로 요청을 보낼 때 CSRF 공격을 막기 위해서는 토큰을 요청에 (헤더나 파라미터 등 안에) 포함시켜서 보내야 한다. 폼 요청 시에는 다음과 같이 히든 필드를 사용한다.

<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

 

타임리프나 스프링 폼 등을 사용하면 간편하게 토큰을 삽입할 수 있다. 만약 이런 기술들을 사용하지 않는다면 요청 속성에 "_csrf"라는 이름으로 토큰이 저장돼 있다는 사실을 이용해 JSTL로 다음과 같이 사용할 수도 있다.

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

 

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

 

Cross Site Request Forgery (CSRF) :: Spring Security

To handle an AccessDeniedException such as InvalidCsrfTokenException, you can configure Spring Security to handle these exceptions in any way you like. For example, you can configure a custom access denied page using the following configuration: Configure

docs.spring.io