Spring Security에서 permitAll(), authenticated() 설정을 마쳤다고 해서 안심할 수 있는 건 아니다. 인증된 사용자의 요청이라도, 그 요청이 정말 본인의 의도인지는 별개의 문제다.
CSRF(Cross-Site Request Forgery) 는 바로 그 지점을 노리는 공격이다. 동작 원리부터 방어 전략까지 정리해 보자.
CSRF, 신뢰를 역이용하는 공격
CSRF(사이트 간 요청 위조)는 브라우저가 사용자의 의사와 관계없이 쿠키를 자동으로 전송하는 편리한 기능을 악용하여, 사용자가 의도하지 않은 요청을 서버로 전송하게 만드는 공격 기법이다.
서버가 가장 신뢰하는 '인증된 사용자'의 권한을 도용한다는 점에서 매우 위협적이다.
CSRF 공격의 다양한 얼굴들 (Attack Vectors)
CSRF 공격은 단순히 <form>을 자동 전송하는 것 외에도 다양한 형태로 이루어질 수 있다.
<form>을 이용한 고전적인 공격
가장 널리 알려진 방식으로, 악성 사이트에 숨겨진 폼을 자바스크립트로 자동 전송하여 상태를 변경하는 POST 요청을 유발한다.
<img>, <script> 등 리소스 태그를 이용한 GET 요청 공격
만약 서버가 GET 요청만으로 중요한 상태 변경(예: 글 삭제, 탈퇴)을 처리하도록 잘못 설계되었다면, 공격은 훨씬 간단해진다.
<!-- 사용자가 이 페이지를 보기만 해도 'deletePost' API가 호출됨 -->
<img src="https://my-site.com/posts/delete?id=123" width="1" height="1" />
공격자는 눈에 보이지 않는 1x1 이미지 태그의 src 속성에 상태 변경 URL을 넣어두는 것만으로도 공격을 성공시킬 수 있다.
교묘하게 위장된 링크(<a>)
CSS를 이용해 악성 링크를 투명하게 만들어 버튼 위에 덮어씌우면, 사용자는 정상적인 버튼을 클릭하는 것처럼 보이지만 실제로는 공격자가 의도한 링크를 클릭하게 된다. (Clickjacking의 일종)
방어 전략 - 다층적 방어(Defense-in-Depth)
하나의 방어벽에만 의존하는 것은 위험하다. 현대적인 웹 애플리케이션은 여러 계층의 방어 전략을 함께 사용한다.
SameSite 쿠키 속성 (1차 방어선)
최신 브라우저는 쿠키의 SameSite 속성을 통해 CSRF 공격에 대한 강력한 내장 방어 기능을 제공한다.
SameSite 속성이란?다른 출처(Cross-Origin)에서 시작된 요청에 쿠키를 보낼지 말지를 결정하는 브라우저 정책이다.
* Lax (대부분 브라우저 기본값)
POST, PUT, DELETE 등 상태를 변경하는 Cross-Origin 요청에는 쿠키를 보내지 않아 대부분의 CSRF 공격을 막아준다.
* Strict
가장 강력한 설정으로, 동일 출처(Same-Origin)에서 보낸 요청에만 쿠키를 전송한다.
SameSite=Lax 정책만으로도 대부분의 CSRF 공격을 막을 수 있지만, 여기에만 의존해서는 안 된다. 오래된 브라우저 지원 문제나, 일부 예외적인 공격 가능성에 대비해 2차 방어선을 구축해야 한다.
CSRF 토큰 - Synchronizer Token Pattern (2차 방어선)
핵심 방어 전략이다. "요청을 보낸 주체가 정말 우리 사이트가 맞는지"를 확인할 수 있는 추가적인 비밀 값을 검증하는 방식이다.
CSRF 토큰을 구현하는 방식 중 하나로, 이 시리즈에서 구현할 패턴이다.
1. 서버는 토큰을 쿠키에 담아 클라이언트에게 전달한다.
2. 클라이언트는 요청 시, 쿠키의 토큰 값을 읽어 HTTP 헤더에 한 번 더 담아 보낸다.
3. 서버는 쿠키의 토큰과 헤더의 토큰이 일치하는지 이중으로 검증한다.
이 방식은 서버가 토큰 상태를 저장할 필요가 없어 Stateless 아키텍처에 매우 적합하다.
심층 Q&A
Q. SOP(동일 출처 정책)가 있는데 어떻게 CSRF 공격이 가능한가?
A. SOP는 다른 출처의 리소스를 읽는 것(Read)을 막는 정책이지, 요청을 보내는 것(Send) 자체를 막지는 않는다. evil-site.com의 스크립트는 my-bank.com의 응답을 읽어올 수는 없지만, my-bank.com으로 요청을 보내는 것 자체는 가능하다. CSRF는 바로 이 점을 이용하는 공격이다.
Q. Referer 헤더를 검증하는 것만으로는 부족한가?
A. Referer 헤더 검증은 여러 한계(개인정보 보호 설정으로 인한 누락, 일부 환경에서의 위변조 가능성) 때문에 CSRF 토큰과 같은 강력한 메커니즘을 대체할 수 없다.
[!WARNING] [자주 하는 실수] GET 요청으로 상태 변경하기
GET 요청은 멱등성(idempotent)을 가져야 한다. 즉, 몇 번을 호출해도 리소스의 상태가 변경되어서는 안 된다. 만약 /posts/delete?id=123과 같이 GET 요청으로 데이터 삭제와 같은 중요한 작업을 처리하도록 API를 설계하면, 위에서 본 <img> 태그 공격처럼 매우 간단한 방법으로 CSRF 공격에 노출된다. 상태를 변경하는 모든 작업은 반드시 POST, PUT, DELETE 메서드를 사용해야 한다.
다음 글에서는 이 이론을 바탕으로, Spring Security에서 CookieCsrfTokenRepository를 사용하여 'Double Submit Cookie' 패턴을 구현하는 방법을 다룬다.