origin과 host, domain, site
- origin
- host
- domain
- site
- same-origin
- same-site
한 줄 결론
주소를 어디까지 잘라서 보느냐의 차이다. https://www.example.com:8443/login에서 이름 한 칸인 www.example.com이 host이고, 여기에 앞의 https와 뒤의 :8443을 묶으면 origin, 반대로 서브도메인과 포트를 지워 example.com까지 줄이면 site다. www.example.com과 login.example.com이 cross-origin이면서 same-site인 것은 재는 자가 두 개라서 나온 결과다.
질문 바로잡기
이 요청이 same-origin인지 same-site인지 묻는 순간 둘 중 하나를 고르는 문제가 된다. 이름이 대칭으로 생겼고 둘 다 "같은 곳에서 온 요청인가"를 묻는 말로 들리니 그렇게 보일 만하다. 그런데 두 이름은 서로 다른 정책이 각자 내린 판정에 붙은 이름이다. 스크립트가 남의 데이터를 읽지 못하게 막는 쪽은 주소를 좁게 보고, 쿠키를 어디까지 딸려 보낼지 정하는 쪽은 넓게 본다. 한 요청에는 두 판정이 동시에 붙고, cross-origin이면서 same-site인 요청은 서브도메인을 쓰는 서비스에서 가장 흔하다.
"도메인이 같은가"로 묻는 습관도 비슷한 함정을 만든다. 실무 대화에서 "도메인이 다르다"는 말은 보통 origin이 다르다는 뜻으로 통하고, 웬만한 상황에서는 그렇게 말해도 결과가 맞아떨어진다. 어긋나는 것은 이름이 그대로인데 origin만 갈리는 주소다. localhost:3000과 localhost:4000처럼 포트만 다르거나 http와 https처럼 스킴만 다른 주소가 그런 경우인데, CORS에 막히거나 저장소가 비어 보이는 일도 대부분 여기서 나온다.
두 함정 모두 주소를 어디까지 잘라서 비교할지 정하지 않은 채 같다, 다르다를 물은 데서 나온다. 그래서 먼저 정해야 할 것은 host, origin, site가 각각 주소를 어디까지 잘라 보는 이름인가다.
무엇으로 갈리는가: 주소를 잘라 보는 범위
세 이름은 같은 주소를 서로 다른 범위로 잘라 본 결과다. host가 재료가 되고, 그 위에 origin과 site가 서로 다른 방식으로 만들어진다. 셋을 한 줄에 나란히 놓으면 관계가 어긋나고, 아래에서 위로 쌓아 올려야 무엇이 무엇을 담는지 보인다.
host는 주소에서 이름이 적히는 칸이다. https://www.example.com:8443/login에서 앞의 https와 뒤의 :8443, /login을 떼면 www.example.com이 남는데 이 부분이 host다. URL 표준은 host를 "도메인, IP 주소, opaque host, 빈 host 중 하나"라고 정의한다. 정의가 형식 나열로 끝나는 것은 host가 보안 정책의 단위가 아니라 문자열을 쪼갠 결과이기 때문이다. 판별도 그만큼 단순해서 scheme과 port와 path를 떼고 남은 이름 하나를 보면 된다.
origin은 그 host에 scheme과 port를 도로 묶은 값이다. 방금 주소라면 https와 www.example.com과 8443 세 조각을 한 덩어리로 본 것이 origin이다. HTML 표준은 두 origin이 같다는 것을 "scheme과 host와 port가 모두 같을 때"로 정의하고, 셋 중 하나라도 다르면 다른 origin이 된다. host를 재료로 삼아 만들어지니 자리도 host보다 한 칸 위다. 브라우저가 스크립트의 DOM 접근과 fetch 응답 읽기, localStorage 저장소를 가르는 단위가 이 칸이다.
site는 origin에서 서브도메인과 포트를 걷어낸 값이다. HTML 표준은 site를 scheme과 등록 가능 도메인으로 계산하는데, 등록 가능 도메인이란 아무나 등록할 수 없는 꼬리에 그 앞 한 칸을 붙인 이름이다. www.example.com이라면 꼬리가 com이고 그 앞 한 칸인 example을 붙여 example.com이 된다. 포트는 계산에 아예 들어가지 않아서 https://example.com과 https://example.com:8443은 origin이 다르면서 site는 같다. 한 site 칸 안에 origin이 여럿 들어가는 구조라서 site가 origin을 담는 바깥 상자가 된다.
두 주소를 비교하는 방법도 이 범위에서 바로 나온다. 두 주소의 scheme과 host와 port를 그대로 맞대 보면 origin 판정이고, host를 등록 가능 도메인까지 줄인 다음 scheme과 함께 맞대 보면 site 판정이다. 뒤쪽 판정에서 스킴을 빼고 등록 가능 도메인만 보면 명세가 schemelessly same site라고 부르는 더 느슨한 판정이 된다.
왜 이렇게 나뉘는가
두 경계는 한 사람이 한 번에 그은 선이 아니다. 서로 다른 시기에 서로 다른 문제를 풀다가 생겼다. 먼저 자리를 잡은 쪽은 쿠키다. 쿠키가 어디까지 딸려 갈지는 Domain과 Path로만 정해지고 스킴과 포트는 처음부터 판정에 들어가지 않았다. MDN은 Domain을 지정하면 "그 도메인과 서브도메인에서 쿠키를 쓸 수 있다"고 설명한다. 로그인 서버와 서비스 서버를 서브도메인으로 나눠 놓고 세션 하나를 함께 쓰려는 요구가 먼저 있었기 때문이다.
origin은 그보다 나중에 격리를 목적으로 정의됐다. RFC 6454는 "스킴을 origin 튜플에 넣는 것은 보안에 필수적이며, 넣지 않으면 http://example.com과 https://example.com 사이에 아무 격리가 없다"고 못 박는다. 같은 회사 주소라도 평문 http로 내려온 문서는 중간에서 내용을 바꿔치기할 수 있으니, 암호화된 문서와 같은 저장소를 공유하게 두면 안 된다는 판단이다. 포트를 튜플에 넣은 것도 같은 이유이고, 한 호스트에서 도는 다른 서비스를 한 덩어리로 보지 않겠다는 선언이다.
넓은 쪽 경계는 계산이 어렵다는 문제를 하나 더 안고 있다. example.com에 쿠키를 심는 것은 자기 서비스 전체에 심는 일이지만, com이나 co.uk에 심는 것은 남의 사이트에까지 심는 일이다. 그런데 이름만 봐서는 그 경계가 어디인지 알 수 없다. 공개 접미사 목록은 "등록 가능한 최상위 수준을 알아내는 알고리즘이 없어서 목록을 만드는 것이 유일한 방법"이라고 설명하고, 실제로 과거에는 .co.uk에 설정한 쿠키가 그 아래 모든 사이트로 전달되는 문제가 있었다. 넓은 경계는 사람이 손으로 관리하는 목록에 기대고 있고, HTML 표준이 "가능하면 same site 검사 대신 same origin 검사를 쓰라"고 권고하는 배경도 이 불안정함이다.
정리하면 두 경계는 막으려는 대상이 다르다. origin은 스크립트가 남의 문서와 저장소를 건드리지 못하게 막는 경계라 세 조각을 그대로 쓴다. site는 쿠키가 어디까지 따라갈지 정하는 경계라 host를 등록 가능 도메인까지 줄여 쓴다. 목적이 달라서 재료를 다르게 골랐고, 그 결과 두 경계의 넓이가 갈렸다.
domain과 hostname은 어디에 놓이는가
domain은 host, origin, site 어느 칸에도 앉지 않는다. URL 표준은 domain을 "네트워크 안의 영역을 식별하는 비어 있지 않은 ASCII 문자열"로 정의하는데, host가 가질 수 있는 여러 형식 가운데 하나를 부르는 이름이다. host 자리에 127.0.0.1이 오면 그것도 엄연히 host지만 domain은 아니다. domain은 범위가 아니라 형식을 가리키는 단어라서, 주소를 얼마나 잘라 보느냐로는 자리를 정할 수 없다.
형식으로 보면 URL은 scheme, host, port, path라는 조각으로 나뉜다. 조각 이름은 문자열을 쪼갠 결과일 뿐이라 보안과 상관이 없고, 파서가 규칙대로 자르면 그대로 정해진다. host 자리에 온 값이 이름이면 domain이고 숫자면 IP 주소라는 구분도 이 층의 이야기다. origin과 site는 이 조각을 골라 담아 만든 경계라서 조각 이름과는 층이 다르다.
hostname도 조각을 부르는 이름인데, 코드에서는 host와 미묘하게 갈린다. URL 표준의 host 개념은 포트를 포함하지 않지만, URL 인터페이스의 host 프로퍼티는 기본 포트가 아닐 때 포트까지 붙여 돌려주고 hostname은 이름만 담는다. new URL('https://example.com:4097')의 host는 example.com:4097이고 hostname은 example.com이다. 명세를 읽다가 코드로 옮길 때 포트가 슬그머니 붙거나 빠지는 일이 여기서 생기므로, 코드에서 이름만 필요하면 hostname을 쓰는 편이 안전하다.
domain이라는 단어는 문맥에 따라 가리키는 대상도 달라진다. 도메인을 등록한다고 할 때의 domain은 레지스트라에서 사는 이름이고, URL 표준의 domain은 host가 이름 형식일 때 부르는 이름이며, 쿠키의 Domain 속성은 전송 범위를 넓히는 설정값이다. 여기에 origin 튜플의 네 번째 자리인 domain 필드까지 있는데, document.domain으로만 건드릴 수 있는 옛 장치다. MDN은 이 방식이 "동일 출처 정책의 보호를 약화시키고 origin 모델을 복잡하게 만든다"며 폐기 대상으로 안내한다. 새 코드에서 domain이라는 단어가 origin과 엮일 일은 없다고 봐도 된다.
경계 사례
로컬 개발 서버 http://localhost:3000과 API 서버 http://localhost:4000을 보자. 포트가 다르니 origin은 확실히 다르고 fetch는 CORS 검사를 받는다. 쿠키 쪽은 사정이 다르다. 애초에 포트를 보지 않는 데다 localhost는 앞에 붙일 칸이 없어 등록 가능 도메인을 만들 수 없는데, 이럴 때는 host를 그대로 site로 쓰기 때문에 두 주소의 site가 같다. 3000번에서 받은 세션 쿠키가 4000번 요청에 딸려 가는데 응답은 CORS로 막히는, 언뜻 앞뒤가 안 맞아 보이는 상황이 여기서 나온다.
https://myapp.github.io와 https://other.github.io는 반대 방향으로 헷갈린다. 이름만 보면 github.io라는 상위 도메인을 함께 쓰니 한 site처럼 보인다. 하지만 github.io가 공개 접미사 목록에 올라 있어서 등록 가능 도메인은 각자 자기 자신이 된다. 판정 결과는 다른 site이고, 덕분에 남이 올린 페이지로 내 쿠키가 새지 않는다. 기준은 상위 도메인을 공유하는지가 아니라 목록이 그은 선의 위쪽인지 아래쪽인지다.
서비스를 http에서 https로 올리는 순간도 판정이 갈리는 지점이다. host는 한 글자도 바뀌지 않았지만 scheme이 튜플에 들어가므로 origin이 통째로 바뀌고, site 계산에도 scheme이 들어가므로 명세 기준으로는 same site도 아니다. 등록 가능 도메인만 보는 schemelessly same site 판정에서만 같다고 나온다. 이 차이 때문에 승격 직후 localStorage가 비어 보이거나 CORS 허용 목록이 한 줄 모자라는 일이 생긴다.
실제로 쓰는 문장
API 서버를 서브도메인으로 나눴을 때
"cross-origin이지만 same-site라서 쿠키는 그대로 가고, 막히는 것은 CORS입니다" 라고 말하면 된다. 하나를 고른 것이 아니라 두 판정이 각각 답했다는 사실이 문장에 드러난다. 그러면 손봐야 할 곳이 쿠키 설정이 아니라 Access-Control-Allow-Origin이라는 것까지 따라온다.
로컬에서 포트만 다른 서버를 붙일 때
"포트가 다르니 origin은 다르고 site는 같습니다" 가 정확하다. "도메인이 같으니 괜찮다"고 말하면 쿠키는 설명되지만 CORS 오류는 설명되지 않아서, 같은 문장으로 두 현상을 한꺼번에 틀리게 만든다.
쿠키 범위를 정할 때
"origin 단위로 막아 주세요" 대신 "Domain 속성을 빼서 그 host에서만 쓰이게 하겠습니다" 라고 말해야 한다. 쿠키에는 origin 단위라는 선택지가 없고 host 단위와 상위 도메인 단위만 있으므로, 앞 문장은 요구 자체가 성립하지 않는다.
배포 주소를 바꿀 때
"host만 바뀌는 변경" 과 "origin이 바뀌는 변경" 을 구분해서 말하는 것이 좋다. 뒤쪽은 저장소와 CORS 허용 목록, 쿠키의 Secure 설정까지 함께 봐야 한다는 신호라서, 같은 배포라도 점검 범위가 달라진다.
사이트라고 말할 때
서비스 하나를 통틀어 사이트라고 부르는 말버릇은 명세의 site와 뜻이 다르다. 명세의 site는 scheme과 등록 가능 도메인으로 계산되는 값이다. 한 회사가 example.com과 example.co.kr을 함께 운영하면 사람 눈에는 한 사이트지만 브라우저에게는 다른 site이고, 쿠키도 그 기준으로 갈린다. 쿠키나 SameSite 동작을 설명하는 자리에서는 "같은 사이트"를 명세의 뜻으로만 써야 듣는 쪽이 헷갈리지 않는다.
근거
- HTML Standard, Origins: https://html.spec.whatwg.org/multipage/browsers.html
- URL Standard: https://url.spec.whatwg.org/
- URL Standard, registrable domain: https://url.spec.whatwg.org/#host-registrable-domain
- RFC 6454, The Web Origin Concept: https://datatracker.ietf.org/doc/html/rfc6454
- web.dev, Understanding "same-site" and "same-origin": https://web.dev/articles/same-site-same-origin
- Public Suffix List, Learn more about the Public Suffix List: https://publicsuffix.org/learn/
- MDN, Using HTTP cookies: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies
- MDN, URL.host: https://developer.mozilla.org/en-US/docs/Web/API/URL/host
- MDN, Same-origin policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy