author
postNo
status
thumbnail
description
category
tags
createdAt
updatedAt
PG(결제대행사)에서 결제를 마치면 서비스의 결제완료 페이지로 돌아온다. 이 페이지의 구성 흐름을 개선하던 중 쿠키의
SameSite 설정을 발견했다. 이 작업을 계기로 외부 결제 화면에서 돌아오는 요청에 기존 쿠키가 어떻게 전달되는지 다시 살펴보았다.먼저 PG에서 결제 성공 URL로 돌아오는 흐름을 보자
결제 성공 URL(payment success URL)은 브라우저가 PG의 결제 화면을 거친 뒤 서비스로 돌아올 주소다. 아래는 PG와 서비스가 서로 다른 사이트에 있고, 브라우저가 최상위 화면으로 복귀하는 상황을 단순화한 예다. 주소와 상태 코드는 설명용이며 실제 연동의 로그를 옮긴 것이 아니다.

- PG 화면에서 결제 절차를 진행한다. 서비스의 세션 쿠키가 브라우저에 저장되어 있어도, PG 사이트의 쿠키와는 별개다.
- PG가 브라우저에 돌아갈 주소를 알려준다. 그림에서는
303 See Other와Location헤더로https://shop.example.com/payment/success를 가리킨다. 브라우저는 이 응답을 보고 다음 URL을 요청한다. MDN: Location 헤더
- 브라우저가 복귀 요청에 넣을 쿠키를 고른다. 이때 요청 대상의 쿠키 범위와
SameSite조건을 확인한다. PG에서 서비스로 넘어오는 cross-site 탐색이므로 기존 Strict 세션 쿠키는 제외된다.
- 브라우저가 결제 성공 URL을 요청한다. 그림의
303복귀는GET /payment/success가 된다. PG 서버가 서비스의 세션 쿠키를 가져와 전달하는 과정이 아니라, 브라우저가 별도의 HTTP 요청을 만드는 과정이다. MDN: 303 See Other
- 서비스 서버가 결제완료 화면을 구성한다. 이 요청에서 세션 쿠키를 전제로 사용자나 주문을 조회한다면, 필요한 인증 정보가 없어 로그인 안내나 조회 실패로 이어질 수 있다.
3번에서 쿠키를 고른 결과가 4번의 요청에 반영된다. 결제 성공 URL로 이동하는 요청 자체는 발생하지만, 화면 구성에 필요한 Strict 쿠키는 그 요청에 포함되지 않을 수 있다. 쿠키가 브라우저에 남아 있는지, 서버가 이번 요청에서 그 쿠키를 받았는지는 구분해야 한다. MDN: SameSite=Strict

이 그림에서 ‘PG 화면의 절차 완료’는 브라우저가 서비스로 돌아오는 시점을 뜻한다. 연동에 따라 결제 승인이나 주문 상태 확정이 별도 단계로 남아 있을 수 있다. 따라서 성공 URL에 도착했다는 사실만으로 결제 상태를 확정하지 않고, 서버에서 확인한 결제 결과와 복귀 요청의 사용자 인증 상태를 각각 판단해야 한다.
실제 복귀 방식은
302, 303, JavaScript를 통한 이동, 폼 POST 등으로 달라질 수 있다. 특히 POST 복귀와 중간 리다이렉트가 있다면 위 GET 예시를 그대로 적용하지 말고 실제 요청의 메서드와 쿠키를 확인해야 한다.이 문제를 출발점으로 쿠키가 저장된 뒤 어떤 조건을 거쳐 서버로 전송되는지 짚어보려 한다. 특히
SameSite=Strict가 외부 유입과 OAuth·결제 복귀에 미치는 영향을 살펴본다. 쿠키로 로그인 상태를 유지하는 웹 서비스 개발자를 대상으로 하며, 본문의 도메인과 코드는 동작 원리를 설명하기 위해 단순화한 예제다.
1. 정확한 표기는 SameSite=Strict다
same-site: strict라는 독립 HTTP 헤더를 설정하는 것이 아니다. 서버가 보내는 Set-Cookie 응답 헤더에 SameSite=Strict 속성을 붙인다.demo는 설명용 값이다. 실제 세션에는 서버가 검증하는 예측 불가능한 식별자 등을 사용한다.브라우저는 쿠키의 값과 속성을 저장하고 이후 요청마다 보낼 쿠키를 고른다. 전송 조건을 만족한 쿠키는 이름과 값이 요청 헤더에 들어간다.
서버는 이 요청 헤더만 보고 쿠키의
SameSite 설정을 알 수 없다. 속성은 서버의 발급 코드와 브라우저 저장소에서 확인해야 한다. MDN의 쿠키 설명
여기서 각 속성의 역할도 구분해두자.
속성 | 하는 일 |
SameSite=Strict | cross-site 요청에서 이 쿠키를 제외한다. |
Secure | 보안 연결을 통한 전송을 요구한다. |
HttpOnly | JavaScript의 직접적인 쿠키 읽기·수정을 제한한다. 브라우저가 요청에 쿠키를 붙이는 동작은 계속된다. |
__Host- 접두사 | 지원 브라우저에서 Secure, Path=/, Domain 생략 조건을 강제한다. |

__Host-를 사용하면 쿠키의 호스트 범위를 좁힐 수 있다. 그렇다고 SameSite의 판정 단위가 origin으로 바뀌는 것은 아니다. MDN: Set-Cookie와 쿠키 접두사2. 같은 사이트는 같은 출처와 다르다
origin은 웹의 출처를 뜻하며 scheme, host, port로 구분한다. site는 일반적인 도메인 URL에서 scheme과 등록 가능 도메인으로 구분한다.
https://app.example.com과 https://api.example.com은 host가 다르므로 cross-origin이다. 하지만 둘 다 HTTPS이고 등록 가능 도메인이 example.com이므로 same-site다. 포트만 달라도 origin은 달라지지만 site는 같다. web.dev: same-site와 same-origin
등록 가능 도메인을 마지막 두 단어로 잘라 구하면 안 된다.
example.co.kr처럼 공용 접미사에 따라 경계가 달라지고, github.io처럼 호스팅 서비스의 경계도 Public Suffix List에 반영된다. alice.github.io와 bob.github.io는 서로 다른 사이트로 취급된다. web.dev의 도메인 경계 예시HTTP와 HTTPS도 구분한다.
http://app.example.com에서 https://app.example.com으로 이동하는 것은 scheme이 달라 cross-site다. 이를 schemeful same-site라고 부른다. 운영 환경은 처음부터 HTTPS로 일관되게 구성해야 판단이 단순해진다. web.dev: Schemeful Same-Site3. Strict는 쿠키 전송 조건 중 하나다
same-site라고 모든 쿠키가 붙지는 않는다. 요청 대상에 맞는 쿠키인지도 확인해야 한다.
예를 들어
app.example.com이 Domain 없이 저장한 쿠키는 그 호스트에만 속하는 host-only 쿠키다. 페이지가 api.example.com을 호출하더라도 이 쿠키가 API 서버로 옮겨가지는 않는다. API 호출에 사용할 쿠키라면 API 호스트에 맞는 발급·저장 구조가 필요하다. MDN: 쿠키의 범위
다음 그림에는 문제를 좁힐 때 살펴볼 점검 항목을 담았다. 나열된 순서가 구현 내부의 알고리즘 순서를 뜻하지는 않는다.

Domain, Path, 만료, Secure, SameSite, 요청 API의 자격 증명 옵션, 브라우저의 쿠키 차단 정책은 각각 확인해야 한다. Path는 쿠키를 어느 요청 경로에 보낼지 정하는 범위이며, 서로 다른 애플리케이션을 안전하게 격리하는 장벽으로 사용하면 안 된다. MDN: Set-Cookie 속성또한 iframe은 주소 두 개만 비교해서 결론 내리기 어렵다. 문서의 쿠키 기준 사이트를 계산할 때는 최상위 문서와 조상 문맥도 함께 고려한다. 아래 설명은 별도 언급이 없으면 일반적인 최상위 문서에서 시작하는 요청을 기준으로 한다. HTTPWG 쿠키 초안 §5.2
4. Strict와 Lax의 차이는 외부 링크에서 드러난다
Strict는 다른 사이트의 문서에서 시작한 요청에 쿠키를 보내지 않는다. 사용자가 직접 클릭한 정상 링크여도 외부 사이트에서 시작한 첫 요청이라는 점은 같다.Lax는 cross-site라도 최상위 탐색이면서 안전한 HTTP 메서드를 사용하는 경우에 예외를 둔다. 대표적으로 링크를 눌러 새 페이지로 이동하는 GET 요청이다. 여기서 최상위 탐색은 iframe 내부 로드나 백그라운드 fetch와 구분되는 페이지 이동이다. web.dev: SameSite cookies explained
표의 ‘허용’은 SameSite 조건을 통과한다는 뜻이다. 실제 전송은 나머지 조건까지 만족해야 한다.
None에는 Secure가 필요하며, None; Secure라도 브라우저가 타사 쿠키를 차단하면 사용할 수 없는 경우가 있다. MDN: 타사 쿠키와 SameSite
주의할 점이 두 가지 있다.
- GET은 상태를 조회하는 용도로 설계해야 한다.
GET /delete-account처럼 서버 상태를 바꾸는 API는Lax의 링크 예외와 충돌한다.
SameSite생략과 명시적인SameSite=Lax를 동일하게 테스트하면 안 된다. 일부 브라우저의 기본값 처리에는 최근 생성된 쿠키를 cross-site POST에 보내는 약 2분의 예외가 있다. 명시적Lax의 일반 규칙으로 확대해서는 안 된다. MDN: Lax와 기본값 예외
리다이렉트나 새로고침을 거치면 쿠키가 붙을까?
자동 리다이렉트 체인에서 이어지는 요청은 구분해서 봐야 한다. 최상위 문서에 도착한 뒤 내부 링크를 눌러 시작하는 새 요청과는 다르다. 내부 링크를 누른 새 요청은 same-site가 될 수 있지만, 리다이렉트를 한 번 더 거쳤다는 이유만으로 Strict 쿠키가 전송된다고 가정해서는 안 된다.
브라우저 UI의 새로고침에도 별도 규칙이 있다. HTTPWG 초안에 따르면 최초 탐색이 cross-site였을 때는 UI 새로고침을 하더라도 처음 제외한 쿠키를 곧바로 전송하지 않도록 한다. 실제 지원 브라우저에서 리다이렉트 각 단계의 요청과 쿠키를 확인해야 한다. HTTPWG 쿠키 초안 §8.8.5
5. credentials: "include"를 넣으면 해결될까?
다음은
https://app.example.com에서 https://api.example.com을 호출하는 예다. 두 주소는 same-site이지만 cross-origin이다.fetch의 기본 자격 증명 모드는 same-origin이다. 위처럼 다른 origin으로 쿠키를 포함하려면 include가 필요하다. 쿠키는 API 호스트 범위에 맞아야 하고, SameSite 등 나머지 조건도 만족해야 한다.호출 측 JavaScript가 응답을 읽을 수 있으려면 API 서버는 응답 헤더에 정확한 origin과 자격 증명 허용 여부를 명시해야 한다.
허용 목록에 따라 origin을 동적으로 응답한다면
Vary: Origin으로 캐시를 구분한다. 요청 origin을 그대로 응답에 넣지 말고 허용 목록으로 검증한다.
credentials: "include"는 SameSite를 무시하라는 명령이 아니다. https://other.example.org에서 호출한다면 위 옵션을 넣어도 기존 Strict 쿠키는 cross-site fetch에 실리지 않는다. CORS를 허용해도 이 조건은 달라지지 않는다.
CORS 오류를 요청 차단과 동일하게 취급해서도 안 된다. 단순 요청은 서버에 도달한 뒤 응답 읽기만 제한될 수 있다. 사전 요청인 preflight가 필요한 호출은 서버가 이를 허가해야 브라우저가 실제 요청을 보낸다. 따라서 CORS만으로 CSRF 방어가 완성되지는 않는다. MDN: Fetch의 자격 증명과 CORS
6. OAuth 로그인과 결제 복귀에서 무엇이 달라질까?
OAuth로 외부 인증 제공자에 갔다가 돌아오는 브라우저 요청도 확인해야 한다. 여기서는 인증 제공자와 서비스가 cross-site라고 가정한다.
구현에 따라 서비스는 로그인을 시작할 때 거래 정보를 저장하고 짧은 시간 동안 유지한다. 콜백의
state와 원래 브라우저의 거래를 연결하거나 PKCE 검증에 필요한 정보를 찾는 과정에서 쿠키에 의존할 수 있다. 이 쿠키가 Strict라면 cross-site 콜백에 기존 쿠키가 실리지 않아 거래를 찾지 못할 수 있다. 모든 OAuth 구현이 같은 방식으로 쿠키를 쓰는 것은 아니다.
최상위 GET 콜백
설계상 브라우저 거래 쿠키가 필요하다면
Lax를 검토할 수 있다. 주 세션과 로그인 거래 쿠키를 분리하면 각 쿠키에 필요한 전송 범위를 따로 정할 수 있다.예를 들어 거래 쿠키의 수명을 짧게 정하고 일회성으로 사용한다. 서버에 저장한 거래와 콜백을 대조한 뒤에는 소비한 것으로 처리한다.
state와 PKCE를 올바르게 검증하고 인증이 완료된 다음 주 세션을 발급·회전한다. PKCE는 인가 코드 교환을 보호하는 장치이며, 단순히 쿠키 옵션을 바꾸는 것과 역할이 다르다. 실제 사용 방식과 CSRF 방어 조건은 OAuth 보안 권고와 사용 중인 라이브러리를 함께 확인해야 한다. RFC 9700: OAuth 2.0 보안 권고form_post 콜백
response_mode=form_post는 브라우저가 HTML 폼을 POST해 인가 응답을 서비스로 전달하는 방식이다. cross-site POST이므로 명시적인 Lax 쿠키에도 최상위 GET 예외가 적용되지 않는다. OAuth Form Post Response Mode필요하다면 거래 쿠키에 한해
SameSite=None; Secure를 검토하거나 검증된 라이브러리가 제공하는 브라우저·거래 결합 방식을 사용해야 한다. 오류를 없애려고 state 검증을 생략하거나 URL의 state 값이 있다는 이유만으로 로그인 요청의 주체를 신뢰해서는 안 된다. RFC 9700: CSRF 방어결제 서비스에서 브라우저가 돌아오는 경로도 같은 방식으로 GET인지 POST인지 확인한다. 다만 서버 간 webhook에는 브라우저의 SameSite 판정이 적용되지 않는다. 브라우저 복귀와 서버 통지를 구분하고 결제 확정은 서버 검증 결과를 기준으로 처리해야 한다.

7. 어떤 조건에서 Strict를 선택할까?
나는 어떤 요청에 인증 정보가 반드시 필요한가를 기준으로 판단한다. 이름이 더 엄격해 보인다는 이유로 선택하지는 않는다.
서비스 조건 | 우선 검토할 선택 | 함께 지불하는 비용 |
외부 링크의 첫 화면부터 로그인 상태를 보여줘야 한다. | Lax와 서버의 CSRF 방어 | GET의 안전성을 지키고 상태 변경 요청을 검증해야 한다. |
외부 진입 시 인증을 바로 이어가지 않아도 된다. | Strict | 익명 진입 화면, 내부 이동, 재인증과 외부 복귀 경험을 설계해야 한다. |
OAuth 거래 쿠키가 콜백에 필요하다. | GET은 Lax 검토, POST는 별도 검토 | 거래와 브라우저를 안전하게 연결하고 만료·일회성·state·PKCE를 검증해야 한다. |
cross-site 임베드에서 쿠키가 필요하다. | 필요한 쿠키에 한해 None; Secure 검토 | 타사 쿠키 정책과 독립적인 인증·CSRF 설계를 검증해야 한다. |

보안이 중요한 관리자 서비스에서도 Strict를 검토할 수 있다. 다만 사내 도메인 구조와 SSO 복귀 방식이 맞아야 한다. 일반 사용자 서비스와 관리자 서비스에 같은 설정을 일괄 적용하기보다 쿠키의 역할과 진입 경로를 나눠 판단하겠다.
결론
브라우저는 저장된 쿠키의 전송 조건을 요청마다 다시 검사한다. 요청 대상의 범위, 보안 연결, SameSite, 자격 증명 옵션과 브라우저 정책을 모두 만족해야 서버로 전달된다. PG에서 결제 성공 URL로 돌아오는 cross-site 요청은 이 중
SameSite=Strict 조건 때문에 기존 쿠키가 제외되는 사례다.진단할 때는 먼저 쿠키가 저장되어 있는지, 이번 요청에 전송되었는지를 따로 확인한다. 다음으로 site와 origin, 쿠키 범위와 CORS, 브라우저 복귀와 서버 간 통지를 구분해야 한다.
나는 외부 링크에서 로그인 상태가 자연스럽게 이어져야 하는 서비스라면 Lax와 적절한 CSRF 방어를 먼저 검토하겠다. Strict는 외부 진입 시 쿠키가 없어도 제품 흐름이 성립하는 경우에 선택하겠다. 적용 전에는 특히 외부 링크와 OAuth·결제 복귀의 실제 요청을 확인해야 한다.