쿠키의 SameSite 속성과 CSRF
쿠키 SameSite 속성 정리하기
2026.11.01
목차
배경
쿠키의 속성을 보면 SameSite라는 설정이 있는데 정리한번 하려고 한다. 이 속성은 다른 사이트에서 시작된 요청에 쿠키를 첨부할지 제어하며, CSRF 공격을 방어하는 데 사용된다.
CSRF는 어떻게 발생할까?
CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 사용자가 로그인한 상태를 악용해, 사용자가 의도하지 않은 요청을 보내게 만드는 공격이다.
예를 들어 다음과 같은 상황을 생각해볼 수 있다.
- 사용자가 은행 사이트에 로그인한다. 브라우저에는 로그인 상태를 식별하는 세션 ID 등의 쿠키가 저장된다.
- 사용자가 로그인 상태를 유지한 채 의도치 않게 악성 사이트에 접속하게 된다.
- 악성 페이지의 스크립트가 사용자의 브라우저로 하여금 은행 서버에 송금 POST 요청을 보내게 한다.
<!-- 은행 서버로 제출되는 폼 -->
<form id="csrf-form"
action="https://bank.example/transfer"
method="POST">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="100000">
</form>
<script>
// 이 페이지를 열면 브라우저가 은행으로 POST 요청을 보냄
document.getElementById("csrf-form").submit();
</script>- 이 요청에 은행 로그인 쿠키가 첨부되고, 은행 서버가 별도의 검증 없이 처리한다면 의도하지 않은 송금이 발생할 수 있다.
여기서 중요한 점은 요청을 보내는 주체가 사용자의 브라우저라는 것이다.
악성 서버
│ 악성 HTML과 스크립트를 내려줌
▼
사용자의 브라우저
│ 은행 서버로 송금 요청
│ 조건에 따라 은행 로그인 쿠키 첨부
▼
은행 서버
공격자가 쿠키 값을 직접 알아야 하는 것은 아니다. 브라우저가 요청에 맞는 쿠키를 자동으로 첨부하는 동작을 악용한다.
물론 다른 사이트에서 시작된 요청에 쿠키가 항상 붙는 것은 아니다. 이때 쿠키 전송을 제한하는 설정 중 하나가 SameSite다.
Domain과 SameSite는 무엇이 다를까?
Domain은 요청의 목적지를 기준으로 쿠키의 전송 범위를 정한다.
예를 들어 은행 쿠키가 Domain=bank.com으로 설정되어 있다면, bank.com과 그 하위 도메인은 해당 쿠키의 전송 대상이다. 요청이 악성 페이지에서 시작되었어도 목적지가 bank.com이라면 Domain 조건은 충족할 수 있다.
이후 SameSite 설정이 다른 사이트에서 시작된 요청에 쿠키를 첨부해도 되는지 제한한다.
악성 페이지에서 은행으로 요청
↓
Domain 확인
“목적지가 이 쿠키를 보낼 수 있는 주소인가?”
↓
SameSite 확인
“다른 사이트에서 시작한 요청에도 이 쿠키를 붙여도 되는가?”
정리하면 다음과 같다.
Domain: 쿠키를 어디로 보낼 수 있는가?SameSite: 다른 사이트에서 시작한 요청에도 쿠키를 보낼 수 있는가?
참고로 Domain을 생략하면 쿠키를 설정한 정확한 호스트에만 전송할 수 있다. 생략한다고 모든 도메인에 전송되는 것은 아니다.
같은 사이트는 어떻게 판단할까?
SameSite에서 같은 사이트인지 판단하는 기본 기준은 스킴과 등록 가능 도메인이다.
스킴은 http, https를 말하고, 등록 가능 도메인은 bank.com처럼 도메인을 등록하는 단위를 말한다.
예를 들어 다음 두 주소는 same-site다.
https://www.bank.com
https://api.bank.com
서브도메인은 다르지만 스킴이 https로 같고, 등록 가능 도메인도 bank.com으로 같기 때문이다.
반면 다음 두 주소는 스킴이 달라 cross-site다.
http://bank.com
https://bank.com
| 비교 대상 | 판단 |
|---|---|
| https://www.bank.com과 https://api.bank.com | same-site |
| https://bank.com과 https://evil.com | cross-site |
| http://bank.com과 https://bank.com | cross-site |
| https://bank.com:3000과 https://bank.com:8080 | same-site |
포트는 same-site 판단 기준에 포함되지 않는다. 따라서 same-site와 same-origin은 다른 개념이다. Same-origin은 스킴, 호스트, 포트가 모두 같아야 한다.
또한 실제 소유주가 같은지를 확인하는 것도 아니다. 브라우저는 주소와 Public Suffix List를 기준으로 판단한다.
예를 들어 github.io는 Public Suffix List에 등록되어 있어서, my-blog.github.io와 your-blog.github.io는 각각 별도의 등록 가능 도메인으로 취급된다. 따라서 두 주소는 cross-site다.
SameSite의 세 가지 설정
SameSite는 다른 사이트에서 시작한 요청에 쿠키를 붙여도 되는지 정하는 설정이다. Strict, Lax, None 세 가지 값이 있다.
은행에 로그인한 뒤, 다른 사이트를 방문한 상황으로 생각해보자.
Strict: 다른 사이트에서 시작했다면 붙이지 않는다
SameSite=Strict는 같은 사이트에서 시작한 요청에만 쿠키를 붙인다.
외부 블로그에서 은행 링크를 눌러 이동하더라도, 처음 은행으로 보내는 요청에는 Strict 쿠키가 붙지 않는다.
인증 쿠키가 Strict라면, 이 첫 요청에서 은행 서버가 로그인 상태를 인식하지 못할 수 있다. 쿠키가 삭제되거나 로그아웃된 것은 아니다.
Lax: 다른 사이트에서 링크로 들어오는 경우는 허용한다
SameSite=Lax도 기본적으로 같은 사이트에서 시작한 요청에 쿠키를 붙인다.
다만 다른 사이트에서 링크를 눌러 은행 페이지로 이동하는 경우에는 예외적으로 쿠키를 붙일 수 있다.
예를 들어 외부 블로그에 이런 링크가 있다고 해보자.
<a href="https://bank.com/account">은행 계좌 보기</a>
이 링크를 누르면 탭 전체가 은행 페이지로 이동하면서 GET 요청을 보낸다. 이때 은행의 Lax 쿠키는 첨부될 수 있다.
정확한 허용 조건은 다음 두 가지다.
- 탭 전체의 페이지가 이동하는 최상위 내비게이션이다.
- GET 같은 안전한 HTTP 메서드를 사용한다.
따라서 다른 사이트에서 은행으로 요청할 때 다음과 같이 동작한다.
| 상황 | 은행의 Lax 쿠키 |
|---|---|
| 링크를 눌러 은행 페이지로 GET 이동 | 첨부 가능 |
| POST 폼을 제출해 은행 페이지로 이동 | 첨부 안 됨 |
| fetch()로 은행에 요청 | 첨부 안 됨 |
| 현재 페이지 안에 은행 이미지나 iframe을 불러옴 | 첨부 안 됨 |
여기서 쿠키가 첨부되지 않는다는 것은 요청 자체가 전송되지 않는다는 뜻은 아니다.
None: 다른 사이트에서 시작한 요청에도 허용한다
SameSite=None은 다른 사이트에서 시작한 요청에도 쿠키를 붙일 수 있도록 허용한다.
예를 들어 악성 페이지에서 은행으로 POST 요청을 보내더라도, 은행의 None 쿠키가 첨부될 수 있다.
그렇다고 악성 사이트에 은행 쿠키를 넘겨주는 것은 아니다. 요청의 목적지가 은행이므로, 쿠키를 받는 곳도 은행 서버다.
한눈에 비교하기
다른 쿠키 조건을 충족한다고 가정하면 다음과 같다.
| 요청 상황 | Strict | Lax | None |
|---|---|---|---|
| 같은 사이트에서 시작한 요청 | 허용 | 허용 | 허용 |
| 다른 사이트의 링크를 통한 GET 페이지 이동 | 차단 | 허용 | 허용 |
| 다른 사이트에서 보낸 POST 요청 | 차단 | 차단 | 허용 |
| 다른 사이트에서 보낸 fetch() 요청 | 차단 | 차단 | 허용 |
요청 상황별 비교
다음 표는 다른 쿠키 조건을 충족한다는 전제에서 SameSite 설정에 따른 차이를 정리한 것이다.
| 요청 상황 | Strict | Lax | None |
|---|---|---|---|
| same-site 요청 | 허용 | 허용 | 허용 |
| 외부 링크를 통한 최상위 GET 이동 | 차단 | 허용 | 허용 |
| 외부 사이트에서 POST 폼 제출 | 차단 | 차단 | 허용 |
| 외부 사이트에서 이미지·iframe 로드 | 차단 | 차단 | 허용 |
이 표의 Lax는 SameSite=Lax를 명시한 경우를 기준으로 한다. 속성을 생략했을 때 일부 브라우저가 적용하는 기본 정책에는 별도 예외가 있을 수 있다.
SameSite만 설정하면 CSRF를 막을 수 있을까?
SameSite는 CSRF 방어에 도움이 되지만, 이것만으로 모든 상황을 막을 수는 없다.
예를 들어 Lax는 외부 사이트에서 시작한 최상위 GET 이동에도 쿠키를 허용한다. 따라서 서버가 GET 요청으로 송금이나 정보 수정 같은 작업을 처리한다면 문제가 생길 수 있다. 또한 서로 다른 서브도메인도 same-site로 취급될 수 있다.
서버에서도 CSRF 토큰이나 요청의 Origin 검증 등을 적용하고, GET 요청으로 데이터가 변경되지 않도록 설계해야 한다.
이번 내용을 이해하면서 가장 헷갈렸던 부분은 요청을 시작한 페이지와 요청을 받는 서버를 같은 것으로 생각했던 점이다.
악성 페이지를 보고 있어도 브라우저는 은행 서버에 요청을 보낼 수 있다. 그리고 그 요청에 은행 쿠키를 첨부할 수 있는지 제한하는 것이 SameSite의 역할이다.