Cookie
쿠키는 서버 응답의 Set-Cookie 등으로 저장되고, 조건에 맞는 후속 요청의 Cookie 헤더에 브라우저가 첨부하는 작은 상태 값이다. HTTP가 무상태라는 것은 서버가 세션 데이터를 저장할 수 없다는 뜻이 아니다. 브라우저의 세션 식별자로 서버의 로그인 상태를 찾는 식으로 애플리케이션이 상태를 구성한다.
저장과 전송은 별도 조건이다
다음은 실제 토큰이 아닌 가상 세션 식별자의 HTTP 헤더 예시다.
HTTP/1.1 200 OK
Set-Cookie: session=demo-session; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/
Domain을 생략하면 설정한 host에만 전송하는 host-only 쿠키다. 허용된 Domain을 지정하면 하위 도메인까지 범위가 넓어질 수 있다. Path는 전송할 경로를 좁히지만 같은 출처 안의 비밀을 격리하는 보안 경계가 아니다. Max-Age는 초 단위이며 Expires와 함께 있으면 우선한다. 둘 다 없으면 세션 쿠키지만 브라우저의 세션 복원으로 다시 살아날 수 있다. 사용자가 지우거나 저장 정책이 거부할 수도 있다. Set-Cookie
GET /api/profile HTTP/1.1
Host: app.example
Cookie: session=demo-session
쿠키 속성 자체가 이 요청 헤더에 따라붙지는 않는다. 실제 첨부 여부는 host·path·수명·Secure·SameSite·요청의 credentials 설정과 브라우저 정책을 함께 만족해야 한다. 같은 도메인으로 보내는 모든 요청에 항상 첨부되는 것은 아니다.
세 보안 속성이 맡는 일
HttpOnly는 페이지 JavaScript가 쿠키 값을 직접 읽는 것을 막는다. XSS가 사용자의 브라우저에서 인증된 요청을 보내는 것까지 막지는 않는다.Secure는 통상 HTTPS 요청에만 전송하게 한다. localhost 예외가 있으며, 디스크 접근이나 HttpOnly 없는 JavaScript 접근을 막는 암호화 저장 옵션은 아니다.SameSite는 site 관계에 따라 첨부를 제한한다. 다른 조건이 충족돼도 이 제한에 걸리면 쿠키를 보내지 않는다.
SameSite는 origin과 다르다
일반적인 도메인의 schemeful site는 scheme과 등록 가능한 도메인으로 구분한다. https://app.example.com과 https://api.example.com은 cross-origin이면서 same-site다. http://app.example.com과 https://app.example.com은 scheme이 달라 cross-site다. public suffix·IP 주소 등은 단순히 호스트의 마지막 두 조각을 자르는 규칙으로 판정하면 안 된다. same-site 정의
아래 표는 다른 쿠키·credentials·브라우저 조건이 모두 충족된 경우 SameSite 속성만 비교한 것이다.
| 요청 맥락 | Strict | 명시적 Lax | None + Secure |
|---|---|---|---|
| same-site 요청 | 허용 | 허용 | 허용 |
| cross-site 최상위 탐색, 안전한 메서드 | 제한 | 허용 | 허용 |
| cross-site POST 폼 | 제한 | 제한 | 허용 |
| cross-site iframe·이미지·fetch | 제한 | 제한 | 허용 |
Lax 예외는 사용자가 직접 클릭한 GET만의 규칙이 아니다. 최상위 탐색과 안전한 메서드가 기준이며, GET으로 상태를 바꾸면 이 예외가 공격 통로가 된다. None에는 Secure가 필요하지만 third-party cookie 차단을 해제하지는 않는다. 속성을 생략해 적용되는 기본 Lax에는 일부 브라우저에서 생성 후 2분 이내 POST를 허용하는 완화가 있어, 명시적 Lax와 동일하다고 가정하지 않는다. SameSite 속성
서버 설정의 경계
일반적인 세션 쿠키는 Lax를 검토하고, 외부 진입 시 로그인 유지가 필요 없는 경우 Strict를 고려한다. cross-site 사용이 실제로 필요한 경우에만 None을 검토한다. 어느 선택도 CSRF 방어 전체를 대신하지 않는다.
다음은 Django 5.2 설정 발췌다. 세션 쿠키와 CSRF 쿠키를 설정할 뿐 CSRF middleware·검증을 대신하지 않는다. 프레임워크 실행 검증은 포함하지 않는다. Django 설정
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = True
다음은 token과 servlet response가 이미 존재하는 Spring 코드의 발췌다. maxAge(3600)의 단위는 초다. 쿠키를 직접 추가했다고 로그인 검증이 구현되는 것은 아니다. ResponseCookie builder
org.springframework.http.ResponseCookie cookie =
org.springframework.http.ResponseCookie.from("session", token)
.httpOnly(true)
.secure(true)
.sameSite("Lax")
.path("/")
.maxAge(3600)
.build();
response.addHeader("Set-Cookie", cookie.toString());
OAuth token을 반드시 쿠키에 저장한다는 표준은 없다. 서버가 token을 관리하고 브라우저에는 불투명한 세션 식별자만 주는 설계도 있다. JavaScript가 필요한 저장소와 수명 비교는 브라우저 저장소에서 다룬다.