이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • CSRF는 왜 쿠키 기반 인증에서 특히 중요한가?
  • SameSite, CSRF token, Origin 검증은 각각 어떤 역할을 하는가?
  • Spring Security CSRF 설정을 언제 유지하고 언제 예외로 둘 수 있는가?

개요

CSRF는 Cross-Site Request Forgery다.

공격자가 피해자의 브라우저를 이용해 피해자의 인증 상태로 상태 변경 요청을 보내는 공격이다.

핵심은 브라우저가 쿠키를 자동 전송한다는 점이다. 피해자가 로그인해 있으면 공격 사이트에서 만든 요청에도 조건이 맞는 쿠키가 붙을 수 있다.

CSRF는 응답을 읽는 공격이 아니다. 응답을 읽지 못해도 이메일 변경, 결제 요청, 권한 변경 같은 상태 변경이 성공하면 피해가 발생한다.

공격 시나리오

피해자는 https://app.example.com에 로그인해 있다.

공격자는 피해자에게 https://evil.example 링크를 보낸다.

그 페이지에는 자동 제출 form이 있다.

<form action="https://app.example.com/api/account/email" method="POST">
  <input name="email" value="attacker@example.com">
</form>
<script>document.forms[0].submit()</script>

서버가 session cookie만 보고 요청을 처리하면 피해자 이메일이 공격자 주소로 바뀐다.

취약한 요청/응답 예시

POST /api/account/email HTTP/1.1
Host: app.example.com
Cookie: SESSION=victim-session
Origin: https://evil.example
Content-Type: application/x-www-form-urlencoded
 
email=attacker@example.com
HTTP/1.1 200 OK
 
email changed

요청은 인증되어 보이지만 사용자 의도 증명이 없다.

주제별 핵심 판단

  • CSRF는 사용자의 브라우저가 인증 cookie를 자동으로 붙이는 성질을 이용한다.
  • 상태 변경 요청은 method, Origin/Referer, CSRF token, SameSite를 함께 본다.

개선된 요청/응답 예시

CSRF 방어는 여러 층으로 둔다.

  • 개선된 상태 변경 요청은 서버가 발급한 CSRF token과 사용자 session을 함께 검증한다.
  • 실패 응답은 내부 token 값을 노출하지 않고 로그에는 origin과 reason을 남긴다.

방어 설계

기본:
  - CSRF token
  - SameSite=Lax or Strict
  - unsafe method 상태 변경에 Origin/Referer 검증
 
고위험 action:
  - 재인증
  - MFA step-up
  - 변경 알림
  - 감사 로그

CSRF token은 공격자가 알 수 없는 값을 사용자의 요청에 포함하게 만든다.

SameSite는 cross-site cookie 전송을 줄인다. 하지만 브라우저 호환성, top-level navigation, OAuth callback, 서브도메인 구성 때문에 유일한 방어선으로 두면 약하다.

Bearer token을 JavaScript가 Authorization header에 직접 넣는 순수 API는 쿠키 자동 전송 CSRF 위험이 작다. 그러나 token을 cookie에 넣거나 refresh endpoint가 cookie 기반이면 다시 CSRF 모델을 봐야 한다.

Spring/HTTP 예시

쿠키 기반 웹 앱은 기본적으로 CSRF를 켠다.

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/assets/**").permitAll()
            .anyRequest().authenticated())
        .build();
}

CookieCsrfTokenRepository.withHttpOnlyFalse()는 프론트가 CSRF token cookie를 읽어 header로 보내게 하기 위한 선택이다. 이 cookie는 인증 cookie와 역할이 다르다.

상태 변경 요청은 token header를 요구한다.

PATCH /api/account/email HTTP/1.1
Host: app.example.com
Cookie: SESSION=victim-session; XSRF-TOKEN=csrf-token
X-XSRF-TOKEN: csrf-token
Origin: https://app.example.com
Content-Type: application/json
 
{"email":"new@example.com"}

stateless bearer API에서 CSRF를 끄려면 token이 cookie로 자동 전송되지 않는다는 전제를 문서와 테스트로 확인해야 한다.

운영 로그와 감사 지점

level=WARN event=csrf_rejected result=blocked actor_id=1004 action=change_email origin=https://evil.example path=/api/account/email reason=missing_or_invalid_csrf_token trace_id=51a0bb71
level=INFO event=csrf_validated result=allowed actor_id=1004 action=change_email origin=https://app.example.com path=/api/account/email trace_id=51a0bb72

CSRF token 원문은 로그에 남기지 않는다. 검증 결과와 reason만 남긴다.

실전 판단 기준

  • 외부 Origin form submit으로 상태 변경이 실패하는가?
  • GET 요청이 상태를 바꾸면 CSRF 방어가 구조적으로 약해진다.
  • SameSite만 믿지 말고 OAuth, 결제 redirect 같은 예외 흐름을 따로 검증한다.

테스트 포인트

  • CSRF token이 없거나 틀리면 403인가?
  • 정상 Origin과 유효 token으로는 상태 변경이 성공하는가?
  • GET 요청이 상태를 변경하지 않는가?
  • refresh token cookie 기반 endpoint에도 CSRF 또는 Origin 검증이 있는가?
  • CSRF 예외 endpoint 목록이 최소화되어 있는가?

위험 신호!

  • 쿠키 기반 인증인데 전역으로 CSRF를 끈다.
  • SameSite만 믿고 민감 상태 변경에 token 검증이 없다.
  • GET으로 이메일 변경, logout, 결제 같은 상태 변경을 수행한다.
  • CORS를 막았으니 CSRF도 막힌다고 생각한다.
  • CSRF token 원문을 로그에 남긴다.
  • OAuth callback 때문에 SameSite를 넓히고 다른 상태 변경 방어를 보강하지 않는다.

확인 질문

확인 질문

  • CSRF가 쿠키 기반 인증에서 중요한 이유는 무엇인가?
    • 브라우저가 cross-site 요청에도 조건이 맞으면 인증 쿠키를 자동 전송할 수 있기 때문이다.
  • CORS와 CSRF 방어는 왜 다른가?
    • CORS는 응답 읽기 정책이고, CSRF 방어는 피해자 인증 상태로 상태 변경 요청이 수행되는 것을 막는 정책이다.
  • CSRF를 꺼도 되는 API의 전제는 무엇인가?
    • 인증 credential이 브라우저에 의해 자동 전송되지 않고, Authorization header 같은 명시적 방식으로만 전송된다는 점이 검증되어야 한다.

참고 문서