본문으로 건너뛰기
김신건의 로그

[Auth] Session Cookie: HttpOnly, SameSite, Secure flag

· 수정 · 📖 약 2분 · 809자/단어 #session #cookie #auth #security #backend
session cookie, HttpOnly, SameSite, Secure cookie, session ID, cookie attributes

정의

Session Cookie서버가 발급한 임의 ID 를 쿠키에 보관. 매 요청 자동 첨부. 서버 측 세션 store (Redis, DB) 와 짝.

IMPORTANT

JWT 와의 비교: session = stateful, JWT = stateless. 작은-중간 모놀리스에서는 session 이 더 단순 + 즉시 revoke 가능. 분산 환경에서는 JWT.

쿠키 속성

Set-Cookie: SID=abc123; Path=/; Domain=example.com;
            Secure; HttpOnly; SameSite=Lax;
            Max-Age=3600
속성의미
HttpOnlyJS 에서 접근 불가 (XSS 방어)
SecureHTTPS 만
SameSite=Strict|Lax|Nonecross-site 동작 제어
PathURL prefix
Domain도메인 범위
Max-Age / Expires만료
Partitioned (2024+)3rd-party cookie 격리
__Host- prefixSecure + Path=/ + Domain 없음 강제

HTTP 세션 전체 흐름

sequenceDiagram
    autonumber
    participant C as Client
    participant W as WebServer
    participant S as SessionStore

    C->>W: POST /login (id, password)
    W->>S: session.set(SID, userId and expiresAt)
    W-->>C: 200 OK, Set-Cookie: SID=abc123, HttpOnly, Secure
    C->>W: GET /profile (Cookie: SID=abc123)
    W->>S: session.get(SID=abc123)
    S-->>W: userId=42, expiresAt=...
    W-->>C: 200 OK (profile data)
    Note over C,W: 로그아웃
    C->>W: POST /logout (Cookie: SID=abc123)
    W->>S: session.delete(SID=abc123)
    W-->>C: 200 OK, SID cleared (Max-Age=0)

로그아웃은 반드시 서버 store 도 삭제. 쿠키만 지우면 SID 재사용 가능.

SameSite 3 모드

flowchart TB
    Strict["Strict<br/>같은 site 만 cookie 전송"]
    Lax["Lax (기본)<br/>top-level GET 만 허용"]
    None["None<br/>모든 cross-site 허용 (+Secure 필수)"]
    Strict --> S1["가장 안전<br/>외부 링크 접근 시 로그인 풀림"]
    Lax --> L1["보안 + UX 균형<br/>대부분 권장"]
    None --> N1["3rd-party 통합<br/>(예: 임베드 위젯)"]
StrictLaxNone
직접 입력 (주소창)OOO
외부 사이트 링크 클릭XO (top-level GET)O
외부 폼 POSTXXO
<img src="other.com">XXO
iframe POSTXXO

2020 부터 Chrome 의 기본값 = Lax. 명시하지 않은 옛 쿠키도 Lax 적용.

CSRF 공격과 방어

sequenceDiagram
    autonumber
    participant V as Victim
    participant A as AttackerSite
    participant B as BankSite

    V->>B: 로그인 (SID=abc 쿠키 발급)
    V->>A: 악성 사이트 방문
    A-->>V: "<form action='bank.com/transfer' method='POST'> 자동 submit"
    V->>B: POST /transfer (Cookie: SID=abc 자동 첨부, amount=100000)
    Note over B: CSRF 취약하면 정상 처리됨

방어 전략:

  1. SameSite=Lax/Strict: 외부 POST 요청 시 쿠키 전송 차단
  2. CSRF Token: 서버가 생성한 토큰을 폼에 hidden 필드로 삽입, 요청 시 검증
  3. Double Submit Cookie: 쿠키와 헤더에 같은 토큰 → origin 확인 효과
  4. Referer / Origin 헤더 검증: 신뢰할 수 없는 origin 차단
<!-- CSRF token 폼 예시 -->
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="{{ csrf_token }}">
  <input type="number" name="amount">
  <button type="submit">송금</button>
</form>

Session Store 패턴

flowchart LR
    C[Client] -->|SID=abc| Web[Web App 1]
    C2[Client] -->|SID=abc| Web2[Web App 2]
    Web --> Store[(Session Store<br/>Redis)]
    Web2 --> Store

세션 store 옵션:

Store특징
메모리단일 노드, 재시작 시 손실
파일단일 노드
Redis분산 표준
DB (Postgres / MySQL)단순 / 큰 부담
클라이언트 (encrypted cookie)stateless 처럼. Express의 cookie-session, Rails encrypted cookie

구현 예시 (Node.js / Express)

import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,  // 32+ bytes random
  resave: false,
  saveUninitialized: false,
  name: "__Host-SID",  // __Host- prefix 강제 보안
  cookie: {
    httpOnly: true,
    secure: true,           // HTTPS only
    sameSite: "lax",        // CSRF 완화
    maxAge: 1000 * 60 * 30, // 30분
  },
}));
Prefix강제 속성효과
__Host-Secure + Path=/ + Domain 없음가장 강력. 서브도메인 격리
__Secure-Secure 만HTTPS 전송 보장
Set-Cookie: __Host-SID=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

__Host- 사용 시 Domain 속성을 설정하면 브라우저가 쿠키를 무시. Path=/ 강제.

Stateless vs Stateful

항목Session (cookie)JWT
서버 store필수불필요
Revoke즉시 (DB 삭제)만료까지 어려움
Cookie 자동 첨부Bearer header 수동
CSRF위험 (SameSite 로 완화)덜 위험 (헤더면)
XSS덜 위험 (HttpOnly)localStorage 면 위험

Session Fixation

sequenceDiagram
    participant Att as Attacker
    participant S as Site
    participant V as Victim

    Att->>S: GET /login (SID=ATTACK123)
    S-->>Att: 응답
    Att->>V: "이 링크로 로그인해" (SID=ATTACK123 박힌 링크)
    V->>S: 로그인 + SID=ATTACK123 유지
    S->>S: 인증 성공, SID=ATTACK123 가 권한 가짐
    Att->>S: SID=ATTACK123 으로 접근
    Note over Att,S: Victim 의 권한 행사

방어: 로그인 직후 새 SID 발급. 옛 SID 무효화.

보안 체크리스트

✓ HttpOnly                (XSS 방어)
✓ Secure                  (HTTPS 만)
✓ SameSite=Lax 이상       (CSRF 완화)
✓ session 만료 짧게        (15-60분 inactive timeout)
✓ session rotation        (privilege escalation 시 새 SID)
✓ session_id 충분히 랜덤   (128+ bits)
✓ 로그아웃 시 server-side 삭제
✓ __Host- 또는 __Secure- prefix 사용
✓ CSRF token 또는 SameSite=Strict

흔한 함정

WARNING

  1. HttpOnly 없음 = XSS 1줄로 세션 탈취.
  2. Secure 없음 = HTTP 구간에서 평문 노출.
  3. SameSite 미설정 = 옛 브라우저는 None 처럼 동작 → CSRF.
  4. 세션 영구 = Max-Age 없이 영구 쿠키. 사용자 비활성 후에도 위험.
  5. 로그아웃이 cookie 만 삭제 = server-side store 에 그대로. 토큰이 재사용 가능.
  6. Session Fixation 미방어 = 로그인 후 SID 교체 안 하면 공격자가 사전에 SID 심기 가능.

CHIPS (Cookies Having Independent Partitioned State): 3rd-party 쿠키 퇴장 이후 임베드 시나리오를 위한 대안.

Set-Cookie: __Host-widget_session=xyz; Secure; HttpOnly; SameSite=None; Partitioned
  • 쿠키가 top-level site 별로 격리. a.com 에서 설정한 widget.com 쿠키는 b.com 에서 보이지 않음
  • Chrome 118+ 기본 지원, Firefox 지원 예정
  • 기존 SameSite=None 임베드 쿠키의 미래 대안

Redis 세션 만료 패턴

// 비활성 만료: 활동 시마다 TTL 갱신
async function touchSession(sid: string) {
  const INACTIVE_TIMEOUT = 30 * 60; // 30분
  await redis.expire(`session:${sid}`, INACTIVE_TIMEOUT);
}

// 절대 만료: 최초 설정 후 고정
async function setSession(sid: string, data: object) {
  const ABSOLUTE_TIMEOUT = 8 * 60 * 60; // 8시간
  await redis.setex(`session:${sid}`, ABSOLUTE_TIMEOUT, JSON.stringify(data));
}

비활성 timeout + 절대 timeout 을 모두 적용하는 것이 정통. 비활성으로는 짧게 (30분), 절대값으로는 길게 (8시간).

관련 위키

이 글의 용어 (5개)
[Auth] CSRF: 위조 요청 공격과 방어auth-security
정의 CSRF (Cross-Site Request Forgery) 는 사용자가 로그인된 상태 에서 공격자가 만든 페이지 가 피해자 브라우저로 위조 요청 을 보내는 공격. 쿠키 기…
[Auth] JWT: 구조, 서명, 만료, refresh tokenauth-security
정의 JWT (JSON Web Token) (RFC 7519) 은 base64url 인코딩된 JSON + 서명 형태의 self-contained token. 서버가 세션 저장 없…
[Network] CORS: Same-Origin Policy, preflight, credentialsnetwork
정의 CORS (Cross-Origin Resource Sharing) 는 Same-Origin Policy (SOP) 의 완화 메커니즘. 브라우저가 origin 이 다른 리소스…
[Redis] Cache Patterns: Cache-Aside, Stampede, Evictiondatabase-internals
정의 Cache Pattern 은 Redis (또는 다른 캐시) 를 어떤 경로로 읽고 쓰는지 의 디자인 결정. 같은 인프라로도 패턴 선택에 따라 일관성 / 지연 / 부하 가 완전…
Sticky Sessionconcurrency
정의 Sticky Session (= Session Affinity)은 로드밸런서가 같은 클라이언트의 요청을 항상 같은 백엔드 서버로 라우팅하도록 하는 동작이다. 서버, 즉 세션…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기