이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Set-Cookie가 내려왔는데 왜 저장되거나 전송되지 않을 수 있는가?
  • Domain, Path, Secure, HttpOnly, SameSite를 백엔드 API 설계에서 어떻게 정하는가?
  • 쿠키 장애를 브라우저와 서버 로그에서 어떤 순서로 확인하는가?

개요

Cookie는 브라우저가 관리하는 HTTP 상태 저장 장치다. 서버는 Set-Cookie로 쿠키를 제안하지만, 저장과 전송 여부를 최종적으로 결정하는 쪽은 브라우저다. 그래서 백엔드 로그에는 로그인 성공이 찍혔는데 사용자는 다음 요청에서 로그아웃된 것처럼 보이는 일이 생긴다.

  • Set-Cookie는 일반 응답 header와 다르게 여러 개가 독립적으로 내려갈 수 있다.
  • HttpOnly는 JavaScript 접근을 막지만 요청 전송 자체를 막지는 않는다.
  • Secure는 HTTPS 요청에서만 쿠키를 전송하게 한다.
  • DomainPath는 쿠키가 붙는 host와 URL 범위를 제한한다.
  • Max-AgeExpires는 만료 정책이고, session cookie는 브라우저 종료 정책에 영향을 받는다.

원리

브라우저는 응답의 Set-Cookie를 받은 뒤 다음 조건을 본다.

  • 현재 응답 origin이 쿠키 Domain을 설정할 권한이 있는가?
  • Secure 쿠키인데 HTTPS가 아닌가?
  • SameSite=None인데 Secure가 빠졌는가?
  • Domain, Path, 만료일이 유효한가?
  • 브라우저의 third-party cookie 차단 정책에 걸리는가?

전송 시에는 요청 URL의 host/path, HTTPS 여부, same-site/cross-site 맥락을 다시 계산한다. 즉 “발급 조건”과 “전송 조건”을 따로 봐야 한다.

속성백엔드 판단 기준자주 나는 장애
HttpOnly세션/refresh token처럼 JS가 읽을 필요 없는 인증 쿠키에 사용프론트가 cookie 값을 직접 읽는 설계와 충돌
Secure운영 인증 쿠키는 기본적으로 사용TLS termination 설정 오류로 app이 HTTP라고 판단해 누락
SameSitesame-site만 필요하면 Lax 우선, cross-site 인증 흐름이면 None; Secure 검토OAuth/결제 redirect 후 쿠키 미전송
Domain가능하면 host-only cookie로 좁게 유지.example.com처럼 넓게 잡아 서브도메인 탈취 영향 확대
PathAPI 범위가 명확할 때 제한/auth로 발급한 쿠키가 /api 요청에 안 붙음
Max-Age서버 session TTL과 일치시킴브라우저 쿠키는 남았지만 서버 session은 만료

Domain을 생략하면 host-only cookie가 된다. 예를 들어 api.example.com에서 발급한 host-only cookie는 www.example.com으로 전송되지 않는다. 반대로 Domain=.example.com은 여러 서브도메인에 붙을 수 있어 편하지만, 공격 표면도 넓어진다.

백엔드 개발자는 “프론트와 API가 서로 다른 서브도메인인가”를 확인하고 cookie scope를 정해야 한다. 도메인을 넓히는 것은 편의가 아니라 보안 경계 변경이다.

코드와 명령으로 확인하기

Spring에서 인증 쿠키를 명시적으로 만들 때의 예시다.

ResponseCookie cookie = ResponseCookie.from("sid", sessionId)
    .httpOnly(true)
    .secure(true)
    .sameSite("Lax")
    .path("/")
    .maxAge(Duration.ofHours(2))
    .build();
 
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

CLI로는 서버가 무엇을 내려보내는지 확인할 수 있다.

curl -i https://api.example.com/login \
  -H 'Content-Type: application/json' \
  --data '{"email":"me@example.com","password":"secret"}'

브라우저 저장 여부는 CLI만으로 확정하지 말고 DevTools의 Application 탭과 Network 탭의 Set-Cookie warning을 함께 본다.

브라우저에서 확인할 증거

  • Network 탭의 login 응답에 Set-Cookie가 있는가?
  • 해당 Set-Cookie에 warning 또는 blocked reason이 있는가?
  • Application 탭에 쿠키가 실제 저장됐는가?
  • 다음 API 요청의 request header에 Cookie가 붙었는가?
  • 요청 URL이 cookie Domain/Path/Secure/SameSite 조건을 만족하는가?

이 다섯 가지를 순서대로 보면 “로그인은 성공했는데 세션이 없다”를 훨씬 빠르게 좁힐 수 있다.

인프라 협업 포인트

  • TLS termination 뒤에서 앱이 Secure cookie를 제대로 만들려면 forwarded proto 설정이 맞아야 한다.
  • CDN이나 WAF가 Set-Cookie를 cache하거나 제거하지 않는지 확인한다.
  • 여러 도메인과 서브도메인을 쓰는 경우 DNS, TLS, cookie Domain, CORS allow origin을 같은 표로 맞춘다.
  • 인프라 팀은 브라우저 저장 정책까지 보지 못하는 경우가 많으므로 앱 팀이 DevTools 증거를 제공해야 한다.

실전 팁

  • 인증 쿠키는 기본적으로 HttpOnly; Secure; SameSite=Lax에서 시작하고, cross-site 흐름이 필요한 경우에만 좁은 origin과 함께 SameSite=None을 검토한다.
  • Domain=.example.com은 SSO에는 편하지만 모든 서브도메인의 보안 수준이 인증 경계가 된다.
  • cookie 크기는 요청마다 전송되는 비용이다. session id만 담고 권한/프로필을 과하게 넣지 않는다.
  • 로그에는 cookie 전체 값을 남기지 말고 cookie 존재 여부, session id hash, user id, request id 정도만 남긴다.
  • 개인 프로젝트에서도 local/prod cookie 설정을 분리한다. Secure 쿠키는 http://localhost 테스트에서 다르게 보일 수 있다.

위험 신호!

  • 인증 쿠키가 HttpOnly 없이 JavaScript에서 읽힌다.
  • 운영에서 Secure가 빠진 session cookie를 쓴다.
  • 서버 session TTL과 cookie Max-Age가 서로 맞지 않는다.
  • 로그인 장애 보고에 Set-Cookie와 다음 요청의 Cookie 비교가 없다.

확인 질문

확인 질문

  • Set-Cookie가 응답에 있는데 다음 요청에 Cookie가 없다면 무엇을 비교해야 하는가?
    • 쿠키 저장 여부, rejected reason, 요청 URL의 Domain/Path/Secure/SameSite 조건, credentials 설정을 비교해야 한다.
  • Domain을 넓게 잡으면 어떤 위험이 커지는가?
    • 하위 도메인 하나의 취약점이나 탈취가 전체 인증 쿠키 경계에 영향을 줄 수 있다.
  • HttpOnly가 막는 것과 막지 못하는 것은 무엇인가?
    • JavaScript의 쿠키 읽기는 막지만 브라우저가 조건에 맞는 요청에 쿠키를 자동 전송하는 것은 막지 못한다.

참고 문서