본문 바로가기
빌드노트 검색

빌드노트

Set-Cookie는 보이는데 __Host- 쿠키가 저장되지 않는다

Set-Cookie가 보여도 __Host- 속성 계약을 어기면 쿠키 저장은 거절된다. 새 Chromium 컨텍스트에서 Path·Domain·Secure 조건별 저장과 다음 요청 헤더를 비교하고, 일반 Path 전송 제한 및 localhost 예외와 구분해 확인할 설정을 설명한다.

응답의 Set-Cookie에 __Host- 쿠키가 보이는데 브라우저 저장 목록에는 없다면, 이름 접두사가 요구하는 속성부터 확인한다. __Host-를 지원하는 브라우저는 Secure, 명시적인 Path=/, Domain 생략 조건을 검사한다. 하나라도 어긋나면 응답은 정상적으로 받아도 그 쿠키의 저장을 거절할 수 있다. 요청 경로 때문에 이미 저장된 쿠키가 전송되지 않는 문제와는 확인할 단계가 다르다.

서버 설정값보다 실제 응답을 먼저 본다

Chrome 개발자 도구를 연 상태에서 쿠키를 발급하는 요청을 다시 실행한다. Network에서 해당 요청을 선택하고 Headers의 응답 Set-Cookie를 확인한다. 서버 코드의 설정이 맞아 보여도 브라우저가 받은 줄에 Domain이 붙거나 Path가 바뀌어 있는지 봐야 한다.

같은 요청의 Cookies 탭에서 차단 안내 아이콘을 확인하면 거절 이유를 좁힐 수 있다. 요청이 많으면 Network의 More filters에서 Blocked response cookies로 걸러 볼 수 있다. 이 경로는 Chrome의 응답 쿠키 차단 확인 안내 (새 탭에서 열림)에 나와 있다.

그다음 Application → Storage → Cookies에서 해당 origin을 선택해 이름을 찾는다. 저장 표의 Domain 열에 발급 호스트가 표시되는 것은 정상이다. 이 열에 호스트가 있다는 사실과 응답에 Domain= 속성을 명시했다는 사실은 다르다. 속성을 생략한 쿠키도 저장할 때는 대상 호스트를 가진다. 속성 유무는 원래 응답 헤더에서 판단한다. 저장 목록을 여는 위치는 Chrome의 쿠키 조회 문서 (새 탭에서 열림)에서 확인할 수 있다.

마지막으로 발급 뒤에 발생한 요청을 Network에서 선택해 요청 헤더의 Cookie를 본다. 발급 응답의 Set-Cookie, 브라우저 저장 목록, 다음 요청의 Cookie를 차례로 대조해야 어디서 빠졌는지 알 수 있다. document.cookie에 없다는 것만으로 저장 실패라고 판단하지는 않는다. HttpOnly 쿠키는 그 조회에서 보이지 않는다. 또한 브라우저의 fetch()로 응답 Set-Cookie를 읽으려 해도 이 헤더는 스크립트에 노출되지 않는다. MDN의 Set-Cookie 설명 (새 탭에서 열림)

Path를 좁히거나 Domain을 채우면 계약이 달라진다

다음은 접두사에 필요한 속성을 갖춘 합성 응답이다. 자기 코드에서는 이 모양이 브라우저까지 전달되도록 쿠키 생성부의 Path·Domain·Secure 설정을 확인한다.

Set-Cookie: __Host-probe=synthetic; Secure; Path=/

__Host- 쿠키에는 Domain을 현재 호스트로 채워 넣지 않고 속성 자체를 생략한다. 일반 도메인 쿠키와 달리 발급한 호스트에만 묶으려는 조건이기 때문이다. Path도 /account처럼 좁히지 않고 /를 지정한다. 이 요구는 MDN의 쿠키 접두사 문서 (새 탭에서 열림)에 정리돼 있다. 이 한 줄은 접두사 조건을 보여 주는 예시이며, 실제 인증 쿠키에 필요한 HttpOnly·SameSite·만료 정책까지 결정한 설정은 아니다.

같은 200 응답에서 저장 결과가 갈렸다

실제 로그인과 분리한 loopback 서버에서 합성 값 synthetic만 발급했다. Windows 11, Playwright 1.59.0, Chromium 147.0.7727.15에서 조건마다 비어 있는 새 브라우저 컨텍스트를 사용했다. 발급 주소는 http://prefix-lab.localhost:<실험 포트>/set이었다. 아래는 문서의 예상이 아니라 직접 실행한 관찰이다.

모든 조건에서 응답은 200이었고 브라우저가 받은 Set-Cookie에도 입력한 줄이 있었다. 앞의 정상 응답을 기준으로 속성을 하나씩 바꿨다.

  • Secure; Path=/, Domain 생략: 저장 목록에 __Host-probe가 생겼고 /echo와 /account/echo 양쪽 요청에서 서버가 Cookie: __Host-probe=synthetic을 받았다.
  • Path=/account로 변경: 저장 목록이 비었고 두 요청 모두 Cookie 헤더가 없었다. /account/echo로 요청해도 복구되지 않았다.
  • Domain=prefix-lab.localhost 추가: 현재 호스트와 같은 값인데도 저장되지 않았고 두 요청 모두 쿠키가 없었다.
  • Secure 생략: 저장되지 않았고 두 요청 모두 쿠키가 없었다.
  • Path 속성 생략: /set에서 발급했는데도 저장되지 않았고 두 요청 모두 쿠키가 없었다.

거절된 네 조건에서는 브라우저 진단 이벤트의 blockedReasons에 모두 InvalidPrefix가 기록됐다. 저장 여부는 현재 URL에 적용되는 쿠키만 조회하지 않고 컨텍스트의 전체 경로를 조회했다. 다른 경로에 저장된 쿠키를 놓쳐 저장 실패라고 부르는 일을 피하기 위해서다.

대조로 접두사 없는 probe=synthetic; Secure; Path=/account를 발급했다. 이 쿠키는 저장됐으며 /echo에는 빠지고 /account/echo에는 전달됐다. 이것이 Path에 따른 전송 범위 제한이다. 반면 같은 Path의 __Host-probe는 저장 단계에서 거절됐다. 접두사 없는 쿠키에 같은 호스트의 Domain을 넣은 대조도 저장·전송됐으므로, 앞의 Domain 거절을 단순히 호스트 철자가 틀린 결과로 해석하지 않았다.

Path 생략도 따로 주의할 점이다. 일반 쿠키는 /set 응답에서 Path를 생략하면 기본 경로가 /가 되지만, __Host-에는 명시적인 Path 속성이 필요하다. HTTPWG 쿠키 명세 초안의 저장 알고리즘 (새 탭에서 열림)도 Path 속성의 존재와 / 경로를 함께 검사한다. 확인한 문서는 2026년 10월 18일자 작업 초안이며 확정 RFC로 인용한 것은 아니다.

localhost의 허용을 일반 HTTP로 확대하지 않는다

이번 HTTP 실험에서 Secure 쿠키가 저장·전송된 데에는 로컬 주소라는 조건이 붙는다. MDN은 localhost에서 Secure의 HTTPS 요구에 예외가 있음을 설명한다. Secure 속성과 localhost 예외 (새 탭에서 열림)

직접 실행한 Chromium에서는 prefix-lab.localhost와 localhost 모두 정상 조합을 받아들였다. 두 호스트의 페이지에서 window.isSecureContext도 true였지만, 통신 자체는 HTTP였다. 이 값을 TLS 암호화의 증거로 삼을 수는 없다. 또 localhost에서도 Secure 속성을 빼자 거절됐다. 로컬 HTTP를 허용하는 예외가 속성 생략까지 허용하지는 않은 것이다.

일반 서비스에서는 유효한 HTTPS에서 Secure; Path=/를 보내고 Domain을 생략하는 설정을 기준으로 삼는다. 이번에는 준비된 신뢰 인증서의 격리 HTTPS 환경이 없어 그 환경의 수락 결과와 일반 HTTP의 거절 결과를 직접 대조하지 못했다. 인증서 경고나 쿠키 보안 검사를 우회하지 않았으며, 다른 브라우저의 동작도 측정하지 않았다. 접두사 검사를 지원하지 않는 브라우저에는 같은 보장을 기대할 수 없다.

설정을 고친 뒤에는 새 합성 쿠키로 저장 목록과 다음 요청을 다시 확인한다. 저장됐는데 요청에서 빠진다면 이제 그 요청 URL과 전송 조건을 살필 차례다. 저장조차 되지 않았다면 발급 응답의 차단 이유부터 다시 읽는다.

← 전체 글 보기