사용자가 로그인된 상태를 악용해, 사용자 의도와 무관한 요청을 보내게 만드는 공격 (Cross-Site Request Forgery).
어떻게 성립하는가 — 브라우저의 자동 쿠키 전송
- ① 피해자가 bank.com 에 로그인 (세션 쿠키 보유)
- ② 로그아웃하지 않은 채 attacker.com 방문
- ③ 그 페이지에 숨겨진 폼이 있다
- <input type="hidden" name="to" value="공격자계좌">
- <input type="hidden" name="amount" value="1000000">
- ④ 브라우저는 bank.com 으로 요청하면서 세션 쿠키를 자동으로 붙인다
- ⑤ 서버 입장에서는 정상 로그인 사용자의 요청 → 이체 실행 ✗
핵심 — 브라우저는 요청 대상 도메인의 쿠키를 누가 요청을 만들었는지와 무관하게 자동으로 붙인다. 이 편의성이 취약점이 된다.
XSS와 무엇이 다른가 — 자주 혼동된다
| CSRF | XSS | |
|---|---|---|
| 공격 위치 | 다른 사이트에서 | 취약한 사이트 안에서 |
| 악용하는 것 | 사용자의 인증 상태 | 사이트의 스크립트 실행 권한 |
| 응답을 읽나 | 못 읽는다(SOP) | 읽는다 |
| 할 수 있는 것 | 요청 보내기만 | 사실상 무엇이든 |
| 위험도 | 높음 | 더 높음 |
CSRF는 요청만 보낼 뿐 응답을 볼 수 없다. 동일 출처 정책 때문이다. 그래서 "이체"는 가능해도 "잔액 조회 후 유출"은 불가능하다.
XSS가 있으면 CSRF 방어는 무의미하다 — 스크립트로 토큰을 읽어 함께 보내면 되기 때문이다. 그래서 XSS 방어가 우선순위가 높다.
방어 ① — CSRF 토큰
-
① 서버가 예측 불가능한 토큰을 발급해 폼에 심는다
-
② 요청 시 그 토큰을 함께 보낸다
-
③ 서버가 세션에 저장된 값과 대조
-
공격자는 이 토큰 값을 알 수 없다
-
다른 사이트에서 응답을 읽을 수 없으므로(SOP) 토큰을 훔칠 방법이 없다
Double Submit Cookie — 서버 저장 없이, 같은 값을 쿠키와 요청 본문에 동시에 넣어 비교하는 변형. 무상태로 구현할 수 있다.
방어 ② — SameSite 쿠키 (현대의 기본 방어선)
Set-Cookie: SID=abc; SameSite=Lax
| 값 | 동작 |
|---|---|
Strict | 외부 사이트발 요청에 절대 안 붙음. 가장 안전하나 외부 링크 이동 시 로그인이 풀린 것처럼 보임 |
Lax | GET 최상위 이동에만 붙음. POST에는 안 붙음 → CSRF 대부분 차단 |
None | 항상 붙음. Secure 필수 |
현대 브라우저는 Lax가 기본값이라 CSRF 위험이 크게 줄었다.
다만 오래된 브라우저나 SameSite=None이 필요한 구성에서는 여전히 토큰이 필요하다.
방어 ③ — Origin/Referer 검증
요청 헤더의 출처가 우리 도메인인지 확인한다. 간단하지만 프록시·프라이버시 설정으로 헤더가 없을 수 있어 보조 수단으로 쓴다.
언제 CSRF 방어가 필요 없나 — 중요한 판단
CSRF는 "브라우저가 인증 정보를 자동으로 붙인다"는 전제에서 성립한다
- 세션 쿠키 인증 — → CSRF 방어 필요 ✅
- Authorization 헤더의 JWT → 필요 없음
- (헤더는 브라우저가 자동으로 붙이지 않는다. JS가 명시적으로 넣어야 한다)
- JWT를 쿠키에 저장 — → 필요하다! 쿠키인 이상 자동 전송된다
"JWT를 쓰니까 CSRF는 신경 안 써도 된다"는 흔한 오해다. 어디에 저장했는지가 기준이다.
// REST API + Bearer 토큰이면 비활성화가 맞다
http.csrf(csrf -> csrf.disable());
다만 "에러가 나서 껐다" 는 것은 위험하다. 인증 방식을 근거로 판단해야 한다.
GET은 상태를 바꾸면 안 된다
❌ GET /delete?id=1
- <img src="https://site.com/delete?id=1"> 만으로 공격 성립
- SameSite=Lax 로도 GET은 막히지 않는 경우가 있다 상태 변경은 POST/PUT/DELETE로. REST의 안전성 원칙이 보안과 직결되는 지점이다.