이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- SameSite는 CSRF 방어에서 어떤 역할을 하고 어디까지 한계가 있는가?
- OAuth, 결제, 외부 redirect에서 쿠키가 사라지는 이유를 어떻게 판단하는가?
- 브라우저 기반 인증에서 CSRF token, Origin 검증, CORS를 어떻게 구분하는가?
개요
CSRF는 사용자가 로그인한 브라우저를 이용해 공격자가 원치 않는 상태 변경 요청을 보내게 만드는 공격이다. SameSite는 cross-site 상황에서 쿠키 전송을 줄여 CSRF 위험을 낮춘다. 하지만 SameSite는 인증/인가 전체를 대체하지 않고, 토큰 검증과 Origin/Referer 검증 같은 방어와 함께 봐야 한다.
SameSite=Lax는 일반적인 top-level navigation에는 일부 쿠키를 보내지만 cross-site subresource/fetch에는 제한적이다.SameSite=Strict는 보안은 강하지만 외부 링크나 redirect 흐름에서 로그인 경험을 해칠 수 있다.SameSite=None은 cross-site 전송을 허용하므로 반드시Secure와 CSRF 방어를 함께 고려한다.- CORS 허용은 CSRF 방어가 아니다. CORS는 응답 노출 정책이고 CSRF는 요청 발생 자체의 문제다.
원리
SameSite 판단에서 중요한 단어는 origin과 site다.
- Origin은 scheme, host, port 조합이다.
- Site는 eTLD+1 기준의 same-site 여부를 본다.
https://app.example.com과https://api.example.com은 cross-origin이지만 same-site일 수 있다.https://example.com과https://evil.com은 cross-site다.
브라우저는 요청이 same-site인지 cross-site인지 판단한 뒤 cookie의 SameSite 속성에 따라 전송 여부를 결정한다.
SameSite 선택 기준
| 상황 | 권장 출발점 | 주의 |
|---|---|---|
| 일반 웹 로그인 | Lax | 외부 POST redirect가 필요한지 확인 |
| admin, 민감 시스템 | Strict 검토 | 외부 링크 진입 UX 저하 |
| 프론트와 API가 다른 site | None; Secure | CORS credential, CSRF token 필수 |
| OAuth/결제 redirect | 흐름별 테스트 필요 | callback 시 cookie 미전송 가능 |
| iframe 기반 임베드 | None; Secure 필요 가능 | third-party cookie 차단 영향 |
OAuth와 Redirect 함정
OAuth 로그인에서는 사용자가 identity provider로 갔다가 callback으로 돌아온다. 이때 로그인 전 발급한 state cookie나 session cookie가 callback 요청에 붙지 않으면 CSRF state 검증이 실패하거나 로그인 상태가 이어지지 않는다.
확인할 것은 세 가지다.
- callback 요청이 top-level navigation인지 fetch/iframe인지
- state/session cookie의 SameSite 값이 무엇인지
- 브라우저가 third-party cookie를 차단했는지
문제가 생겼다고 바로 SameSite=None으로 풀기보다 흐름의 site 관계와 CSRF 방어를 같이 점검한다.
CSRF 방어 방식
CSRF 방어는 여러 신호를 겹쳐 쓴다.
- SameSite cookie로 기본 cross-site cookie 전송을 줄인다.
- 상태 변경 요청에는 CSRF token을 요구한다.
Origin또는Referer가 허용된 site인지 검증한다.- JSON API는 custom header를 요구해 단순 form POST를 어렵게 만든다.
- 인증 cookie scope를 최소화한다.
Spring Security에서 cookie 기반 CSRF token을 쓰는 예시다.
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);withHttpOnlyFalse()는 프론트가 CSRF token을 읽어 header로 다시 보내야 하는 구조에서 쓴다. session cookie 자체를 JavaScript가 읽게 해도 된다는 뜻이 아니다.
코드와 명령으로 확인하기
브라우저 fetch는 credential 포함 여부와 CSRF header를 명시해야 한다.
await fetch("https://api.example.com/orders", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken,
"X-Request-ID": crypto.randomUUID()
},
body: JSON.stringify({ productId: "p-1", quantity: 1 })
});서버 응답만 CLI로 확인할 때는 Origin과 cookie를 직접 넣는다.
curl -i https://api.example.com/orders \
-H 'Origin: https://app.example.com' \
-H 'Content-Type: application/json' \
-H 'X-CSRF-TOKEN: test-token' \
-b 'sid=test-session' \
--data '{"productId":"p-1","quantity":1}'CLI는 브라우저처럼 SameSite를 자동 판단하지 않으므로 “서버가 어떤 응답을 하는지” 확인하는 용도로만 쓴다.
인프라 협업 포인트
- TLS termination과 forwarded proto가 틀리면
Secure/redirect 판단이 어긋나 SameSite=None 쿠키가 저장되지 않을 수 있다. - WAF가
Origin,Referer, CSRF header를 제거하거나 변경하지 않는지 확인한다. - CDN이 로그인 관련 응답을 cache하지 않도록
Cache-Control과 cookie 포함 응답 정책을 확인한다. - SSO/OAuth 도메인, callback 도메인, cookie Domain, CORS allow origin을 한 장의 흐름도로 맞춘다.
실전 팁
- CSRF는 “쿠키 인증을 쓰는 상태 변경 요청”에서 특히 중요하다. Bearer token을 Authorization header에 넣는 구조라도 token 저장 위치가 cookie면 다시 CSRF를 봐야 한다.
SameSite=None은 해결책이 아니라 더 넓은 전송 허용이다. 반드시Secure, Origin 검증, token 방어와 함께 쓴다.- OAuth state 검증 실패는 보안 기능이 정상 동작한 결과일 수 있다. 실패를 꺼 버리기보다 cookie 전송 조건을 고친다.
- GET 요청에 상태 변경 부작용을 넣으면 SameSite와 CSRF 방어가 약해진다.
- admin 기능은 SameSite만 믿지 말고 재인증, MFA, Origin 검증, audit log를 함께 둔다.
위험 신호!
- CORS allowlist를 넓히면서 CSRF 방어를 강화했다고 착각한다.
- 상태 변경 API가 GET으로 열려 있다.
- OAuth callback 장애를 해결하려고 state 검증을 비활성화한다.
SameSite=Nonecookie가 있는데 CSRF token이나 Origin 검증이 없다.
확인 질문
확인 질문
- CORS가 CSRF 방어가 아닌 이유는 무엇인가?
- CORS는 브라우저가 응답을 JavaScript에 노출할지 정하는 정책이고, CSRF는 인증 쿠키가 붙은 요청이 서버에 도달하는 문제이기 때문이다.
SameSite=None을 쓸 때 반드시 같이 확인해야 하는 것은 무엇인가?
Secure, HTTPS, credential CORS, CSRF token, Origin/Referer 검증을 함께 확인해야 한다.- OAuth callback에서 session cookie가 빠지면 어떤 증상이 날 수 있는가?
- state 검증 실패, 로그인 루프, callback 후 사용자 식별 실패가 발생할 수 있다.