CSRF 공격 방어
Spring Security의 CSRF 보호는 동기화 토큰 패턴(Synchronizer Token Pattern)을 사용한다.
CsrfFilter의 동작 원리
1. 어떤 요청에 대해 적용되나?
- "state-changing" 요청만 검사한다.
- 즉, POST, PUT, DELETE, PATCH 요청 등에 대해 필터가 동작.
- GET, HEAD, TRACE, OPTIONS 요청은 검사하지 X
2. CSRF 토큰 생성 및 저장 (서버 측)
- 사용자가 처음 애플리케이션에 접근하면,
CsrfFilter는 내부적으로 CsrfTokenRepository를 통해 CSRF 토큰을 생성. - 이 토큰은 서버 세션 또는 쿠키에 저장된다.
3. 클라이언트가 요청 시 토큰을 함께 보냄 (클라이언트 측)
- 클라이언트는 이후의 POST, PUT 등의 요청에 CSRF 토큰을 요청 헤더에 담아서 전송해야 한다.
4. 서버가 토큰을 비교하고 일치 여부 확인
- CsrfFilter는 요청에 포함된 토큰과 CsrfTokenRepository에 저장된 토큰을 비교.
- 두 토큰이 일치하면 요청을 허용, 다르면 403 Forbidden 반환.
5. 검사 실패 시 결과
- 로그:
Invalid CSRF token found for request...
- 응답:
HTTP/1.1 403 Forbidden
CsrfFilter 적용하기
가장 기본적인 SecurityFilterChain 등록
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
}
}
CsrfTokenRepository 구현체를 CookieCsrfTokenRepository로 설정
CsrfTokenRequestHandler 구현체를 CsrfTokenRequestAttributeHandler로 설정
현재 내 프로젝트는 CSR(Client-Side Rendering) 방식이므로
클라이언트에서 쿠키에 저장된 CSRF 토큰에 접근해야 한다.
따라서 Http Only는 false로 설정
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
동작 순서 요약
- 사용자가 페이지에 접근 -> 서버가 쿠키로 CSRF 토큰 전달 (XSRF-TOKEN=...)
- 프론트엔드는 document.cookie 등으로 토큰 꺼냄 -> 요청 시 헤더에 추가 (X-XSRF-TOKEN)
- 서버는 CookieCsrfTokenRepository를 통해 저장된 토큰과,
- CsrfTokenRequestAttributeHandler를 통해 요청에서 꺼낸 토큰을 비교
- 일치하면 정상 처리, 아니면 403 오류
각 컴포넌트 설명
1. csrfTokenRepository(...)
- 무슨 컴포넌트?
- CSRF 토큰을 어디에 저장하고, 어떻게 꺼낼지 정의하는 저장소.
- CsrfTokenRepository는 인터페이스이며, 구현체를 바꿔 끼울 수 있음.
- 기본 구현체는?
- HttpSessionCsrfTokenRepository (기본값): 세션에 저장 / 서버 측 HttpSession
- CookieCsrfTokenRepository: 쿠키에 저장 ☜ 현재 내가 사용한 것 / 클라이언트 측 쿠키
2. CookieCsrfTokenRepository.withHttpOnlyFalse()
- 무슨 컴포넌트?
- CSRF 토큰을 브라우저 쿠키에 저장하도록 설정함.
- withHttpOnlyFalse()는 생성된 쿠키에 HttpOnly=false 속성을 붙여 JavaScript에서 읽을 수 있게 만듬.
- 어떤 쿠키?
Set-Cookie: XSRF-TOKEN=abc123xyz; Path=/
- 왜 필요?
- 프론트엔드(JavaScript)가 이 토큰을 읽어서 헤더에 담을 수 있게 하기 위함
3. csrfTokenRequestHandler(...)
- 무슨 컴포넌트?
- 클라이언트가 보낸 요청에서 CSRF 토큰을 어디서 꺼낼 것인지 정의함.
- CsrfTokenRequestHandler는 요청에서 토큰을 읽고 검증하도록 돕는 역할을 함.
4. new CsrfTokenRequestAttributeHandler()
- 무슨 컴포넌트?
- CsrfTokenRequestHandler의 구현체 중 하나.
- 요청 헤더나 폼 필드에서 토큰을 꺼내서, HttpServletRequest attribute (_csrf)에 저장해줌.
- 어떤 작업?
- 이 설정 덕분에 컨트롤러에서 아래처럼 CSRF 토큰을 직접 꺼낼 수 있음:
@GetMapping("/form")
public String form(HttpServletRequest request) {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
return ...;
}