DEV BOX

'26/11/02 UPDATE

WEB / COOKIES / FIELD NOTES

localhost의 포트가 다르면 쿠키도 따로 저장될까

localhost:4371과 localhost:4372에 쿠키 이름이 같은 앱 두 개를 띄우면 한쪽 로그인이 다른 쪽 쿠키를 덮어쓰고, 한쪽에서 로그아웃하면 다른 쪽도 풀립니다. 쿠키는 호스트 이름 단위로 저장되고 포트를 보지 않기 때문입니다. Node.js 서버 두 개를 Chromium 152 기반 브라우저로 열어 남긴 서버 로그로 이를 확인하고, SameSite=Strict와 __Host-로도 나뉘지 않는 이유, 쿠키 이름과 호스트 이름 가운데 무엇으로 나눌지를 정리합니다.

localhost:4371과 localhost:4372에 앱 두 개를 띄워 놓았는데, 한쪽에 로그인하면 다른 쪽 로그인이 풀린다면 쿠키가 포트를 가리지 않기 때문입니다. 브라우저는 두 주소를 서로 다른 출처로 보지만, 쿠키를 저장하고 보낼 때는 포트를 보지 않습니다. 이 글을 읽고 나면 서버 로그의 Cookie 헤더로 이 현상을 직접 확인할 수 있고, 두 앱을 나눌 때 쿠키 이름을 바꿀지 호스트 이름을 바꿀지 고를 수 있습니다.

실험은 2026-10-27 macOS 27.0.1에서 했습니다. Node.js v24.21.0의 node:http로 만든 서버 두 개를 4371과 4372 포트에 띄우고(127.0.0.1과 ::1에서만 열림), Claude 데스크톱 앱의 내장 브라우저(Chromium 152.0.7977.167)로 열었습니다. 서버는 요청마다 Host 헤더와, Cookie 헤더 가운데 이름이 lab1130_나 __Host-lab1130_로 시작하는 실험용 쿠키를 로그에 남기고, 페이지는 자기 localStorage 값을 서버에 알려 같은 로그에 남깁니다. 쿠키 이름 lab1130_sid와 그 값은 실험용입니다. Google Chrome 자체와 Safari, Firefox, HTTPS 주소는 실행하지 않았습니다.

4371에서 받은 쿠키가 4372로 간다

먼저 localhost:4371/login을 열었습니다. 서버는 Domain 속성 없이 아래 헤더를 보냈습니다.

Set-Cookie: lab1130_sid=app-4371; Path=/; Max-Age=600; HttpOnly; SameSite=Lax

이어서 localhost:4372/를 열자 4372 서버의 로그에 Cookie: lab1130_sid=app-4371이 찍혔습니다. 4372는 이 쿠키를 만든 적이 없습니다. 반면 같은 페이지의 localStorage는 비어 있었고, 4372가 넣은 값은 4371로 돌아가도 보이지 않았습니다. 브라우저가 두 주소를 다른 출처로 본다는 것은 맞습니다. 쿠키만 다른 규칙을 따랐습니다.

서버 로그. 4371의 /login이 Set-Cookie: lab1130_sid=app-4371; Path=/; Max-Age=600; HttpOnly; SameSite=Lax를 보냈다. 이어서 GET localhost:4372/ 요청에 Cookie: lab1130_sid=app-4371이 실렸고, 4372 페이지의 localStorage는 before=(none) now=set-by-4372였다. 다시 연 localhost:4371/에도 같은 쿠키가 실렸고 localStorage는 before=set-by-4371 now=set-by-4371이었다.
localhost:4371에서 로그인한 뒤 localhost:4372를 연 서버 로그입니다. 쿠키는 4372로 넘어갔고 localStorage는 포트마다 따로였습니다.

쿠키를 고르는 조건에는 포트가 없다

출처(origin)는 프로토콜, 호스트, 포트 세 가지로 정해집니다. Web Storage와 IndexedDB는 출처마다 따로 저장되므로 localStorage는 포트별로 나뉘었습니다. 쿠키는 출처를 다르게 정의합니다.

쿠키의 규칙을 정한 RFC 6265(2011)에서 Domain 속성 없이 받은 쿠키는 host-only 쿠키가 됩니다. 이 쿠키의 도메인 칸에는 요청한 주소의 호스트 이름이 들어가고, 포트는 들어가지 않습니다. 요청에 쿠키를 붙일 때 맞춰 보는 조건(호스트 이름, 경로, Secure 여부)에도 포트는 없습니다. 그래서 localhost:4371에서 받은 쿠키는 localhost라는 호스트의 쿠키가 되고, 같은 호스트라면 어느 포트로든 갑니다.

RFC 6265도 이것을 약점으로 봅니다. 서론부터 한 호스트의 쿠키는 그 호스트의 모든 포트가 함께 쓴다고 밝히고, 보안 고려 사항 8.5절은 "Cookies do not provide isolation by port."로 시작합니다. 서로 믿지 못하는 서비스를 같은 호스트의 다른 포트에 두면서 민감한 정보를 쿠키에 담아서는 안 된다는 권고(SHOULD NOT)도 같은 절에 있습니다. RFC 6265를 대체할 개정안(rfc6265bis)은 2026-10-27 기준으로 아직 RFC 번호가 없는 인터넷 초안(22판)이고, 이 문장들을 그대로 두었습니다.

같은 이름이면 덮어쓰고, 로그아웃도 함께 된다

이번에는 4372에서 로그인했습니다. 4372가 같은 이름으로 lab1130_sid=app-4372를 보내자 이름, 도메인, 경로가 같은 기존 쿠키가 지워지고 새 쿠키로 바뀌었습니다. 그 뒤 4371로 가는 요청에는 app-4372가 실렸습니다. 세션을 서버마다 따로 저장하는 앱이라면 4371은 자기가 발급하지 않은 세션 ID를 받게 되고, 그 ID를 찾지 못하면 로그인이 풀린 것처럼 보입니다. 반대로 4371에서 로그아웃해 Max-Age=0으로 쿠키를 지우자 4372의 다음 요청에도 쿠키가 없었습니다.

같은 세션 미들웨어의 기본 쿠키 이름을 그대로 둔 앱 둘을 함께 띄우면 이렇게 됩니다. 예를 들어 Express의 세션 미들웨어 express-session은 쿠키 이름의 기본값이 connect.sid입니다. 이 패키지의 README는 같은 호스트 이름에서 앱을 여러 개 돌린다면 앱마다 name을 다르게 주라고 하고, 스킴이나 포트가 다르다고 다른 호스트 이름이 되지는 않는다고 덧붙입니다. 이 실험은 express-session을 쓰지 않고 node:http로 같은 상황을 만들었습니다.

그래서 쿠키 이름을 lab1130_sid_4371과 lab1130_sid_4372로 나눠 다시 해 보았습니다. 덮어쓰기와 함께 로그아웃은 사라졌습니다. 하지만 4371로 가는 요청에는 두 앱의 쿠키가 모두 실렸습니다. 이름이 부딪치지 않을 뿐, 다른 앱의 세션 쿠키를 받는 것은 그대로입니다.

서버 로그. 4372의 /login 요청에 Cookie: lab1130_sid=app-4371이 실렸고 4372는 Set-Cookie: lab1130_sid=app-4372를 보냈다. 이어서 GET localhost:4371/에 Cookie: lab1130_sid=app-4372가 실렸다. 4371의 /logout이 lab1130_sid=; Max-Age=0을 보낸 뒤 GET localhost:4372/는 Cookie: (none)이었다. 앱별 이름으로 바꾼 뒤 GET localhost:4371/에 Cookie: lab1130_sid_4371=app-4371; lab1130_sid_4372=app-4372가 실렸고, 4371의 /logout-named 뒤에도 GET localhost:4372/에는 Cookie: lab1130_sid_4372=app-4372가 남았다.
같은 이름(lab1130_sid)을 쓴 두 앱은 서로의 로그인을 덮어쓰고 로그아웃도 함께 됐습니다. 앱마다 이름을 다르게 주면 그 문제는 사라지지만 쿠키는 여전히 두 앱에 모두 갑니다.

SameSite와 __Host-도 포트는 가르지 않는다

쿠키 속성을 더 엄격하게 주면 나뉠까요. 4371에서 __Host- 접두사를 붙이고 Secure와 SameSite=Strict까지 준 쿠키를 보냈습니다.

Set-Cookie: __Host-lab1130_sid=app-4371; Path=/; Max-Age=600; Secure; HttpOnly; SameSite=Strict

__Host- 쿠키는 Secure와 Path=/가 있고 Domain 속성이 없을 때만 저장되어 한 호스트에 묶입니다. 주소는 http였지만 이 브라우저는 localhost가 설정한 쿠키에 https 요구를 적용하지 않았고, 쿠키는 그대로 저장됐습니다. 그리고 이 쿠키도 4372 요청에 실렸습니다. 포트는 __Host- 쿠키도 여전히 무시하는 유일한 출처 요소입니다(rfc6265bis 22판).

SameSite=Strict는 쿠키를 설정한 사이트와 같은 사이트에서 시작한 요청에만 쿠키를 보냅니다. 그런데 HTML 표준의 같은 사이트 판정은 스킴과 등록 가능한 도메인(없으면 호스트)만 비교하고 포트는 무시합니다. localhost:4372 페이지에서 fetch('http://localhost:4371/probe', {mode: 'no-cors', credentials: 'include'})를 보내자 Strict 쿠키가 실렸고, 요청의 Sec-Fetch-Site 헤더는 same-site였습니다. 같은 요청을 127.0.0.1:4372 페이지에서 보내면 cross-site였고 쿠키가 없었습니다. fetch가 다른 출처로 쿠키를 보내는 조건은 브라우저가 JSON 요청을 막았는데 서버에는 왜 안 보일까에서 다뤘습니다.

서버 로그. 4371의 /login-host가 Set-Cookie: __Host-lab1130_sid=app-4371; Path=/; Max-Age=600; Secure; HttpOnly; SameSite=Strict를 보냈다. GET localhost:4372/에 Cookie: __Host-lab1130_sid=app-4371이 실렸다. localhost:4372 페이지가 보낸 fetch('http://localhost:4371/probe', {mode: 'no-cors', credentials: 'include'})는 같은 쿠키를 싣고 Sec-Fetch-Site: same-site였다. 127.0.0.1:4372 페이지가 보낸 같은 fetch는 Cookie: (none), Sec-Fetch-Site: cross-site였다.
__Host- 접두사와 SameSite=Strict를 붙여도 쿠키는 다른 포트로 갔습니다. 4372 페이지가 4371로 보낸 fetch도 포트만 달라 same-site로 판정되어 쿠키가 실렸습니다.

호스트 이름을 나누면 따로 간다

쿠키가 호스트 이름을 따르니, 앱마다 호스트 이름을 다르게 주면 됩니다. a.localhost:4371에서 로그인한 뒤 b.localhost:4372와 localhost:4372로 가는 요청에는 쿠키가 없었습니다. 포트만 다른 a.localhost:4372로는 쿠키가 갔습니다. localhost와 127.0.0.1도 서로 다른 호스트라 쿠키가 오가지 않았습니다. 다만 127.0.0.1:4371에서 받은 쿠키는 127.0.0.1:4372로 갔습니다. IP 주소로 열어도 포트로는 쿠키가 나뉘지 않습니다.

localhost와 그 아래 이름은 루프백 주소로 풀린다고 보아도 됩니다(RFC 6761). 이 Chromium 기반 브라우저는 /etc/hosts에 항목을 넣지 않아도 a.localhost와 b.localhost를 열었습니다. Safari와 Firefox, 그리고 브라우저 밖의 도구가 이 이름을 어떻게 푸는지는 확인하지 않았습니다.

서버 로그. a.localhost:4371/login이 Set-Cookie: lab1130_sid=app-4371을 보낸 뒤 GET a.localhost:4372/에는 Cookie: lab1130_sid=app-4371이 실렸고, GET b.localhost:4372/와 GET localhost:4372/는 Cookie: (none)이었다. localhost:4371/login 뒤 GET 127.0.0.1:4372/는 Cookie: (none)이었다. 127.0.0.1:4371/login 뒤 GET 127.0.0.1:4372/에는 Cookie: lab1130_sid=app-4371이 실렸고, GET localhost:4372/는 Cookie: (none)이었다. 각 단계 사이의 /logout 줄은 Set-Cookie: lab1130_sid=; Path=/; Max-Age=0이다.
호스트 이름을 나누면 쿠키가 따로 갑니다. 포트만 다른 a.localhost:4372와 127.0.0.1:4372에는 여전히 쿠키가 갔습니다.

이 동작은 앞으로 바뀔 수 있습니다. Chrome Platform Status에는 쿠키를 설정한 출처(포트 포함)에 쿠키를 묶는 'Origin-Bound cookies (by default)' 항목이 있고, 2026-10-27 기준 상태는 Proposed입니다. 위 결과는 Chromium 152 기반 브라우저에서 본 것이며, 이 기능이 켜진 브라우저에서는 결과가 다를 수 있습니다.

쿠키 이름과 호스트 이름 가운데 고르기

개발 노트

쿠키는 호스트 이름, localStorage는 출처로 나뉜다

포트가 달라도 localhost면 쿠키 상자는 하나다

  • 쿠키: 호스트 이름 단위

    localhost:4372 Cookie: lab1130_sid=app-4371

    • 4371에서 받은 쿠키가 4372 요청에 실렸다
    • 같은 이름이면 덮어쓰고 로그아웃도 함께 된다
  • localStorage: 출처 단위

    localhost:4372 before=(none)

    • 출처는 프로토콜·호스트·포트로 정해진다
    • 4372 페이지에는 4371이 넣은 값이 없었다
  • 방법 1: 앱마다 쿠키 이름을 다르게

    lab1130_sid_4371 · lab1130_sid_4372

    • 덮어쓰기와 함께 로그아웃은 사라진다
    • 다른 앱의 쿠키도 여전히 함께 전송된다
  • 방법 2: 호스트 이름을 나누기

    b.localhost:4372 Cookie: (none)

    • a.localhost의 쿠키는 b.localhost로 가지 않았다
    • localhost의 쿠키는 127.0.0.1로 가지 않았다

2026-10-27 Chromium 152.0.7977.167 기반 브라우저와 Node.js v24.21.0 서버 두 개로 실행했다. SameSite=Strict와 __Host- 접두사를 붙인 쿠키도 다른 포트로 갔다.

덮어쓰기와 함께 로그아웃만 막으면 되면 쿠키 이름을, 서로의 쿠키를 받지 않아야 하면 호스트 이름을 나눕니다.

한쪽 로그인이 다른 쪽을 덮어쓰거나 함께 풀리는 것만 막으면 될 때는 앱마다 쿠키 이름을 다르게 줍니다. 두 앱이 서로의 쿠키를 받아서는 안 될 때는 a.localhost와 b.localhost처럼 호스트 이름을 나눕니다. Domain 속성 없이 설정한 쿠키라면 이렇게 나뉩니다. 어느 쪽인지 헷갈리면 서버 로그에 Host와 Cookie를 함께 찍어 보세요. 쿠키는 그것을 만든 앱이 아니라 요청이 향한 호스트 이름을 따라갑니다.

끝 · END OF NOTE
RSS 구독목록으로
COMMENTS BOX

이 기록에 대화를 더해 주세요.

궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.

최신순

로그인 상태 확인 중…

댓글을 불러오는 중…