HTTP Fundamentals

HTTP Cookies: SameSite, Secure, and HttpOnly Explained

How cookies work in HTTP, the role of Set-Cookie headers, and modern cookie attributes (SameSite, Secure, HttpOnly, Partitioned) for privacy and security.

How Cookies Work in HTTP

Cookies are small pieces of state that browsers store and automatically send back with every matching request. They were introduced in 1994 to solve a fundamental problem: HTTP is stateless, yet web applications need to recognize returning users.

The basic exchange works through two headers:

# Server sets a cookie in the response:
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure

# Browser sends it back on every subsequent request:
Cookie: session_id=abc123

Domain and Path Scoping

Cookies are scoped to a domain and path. By default, a cookie set by api.example.com is only sent to api.example.com. Adding Domain=example.com makes it available to all subdomains — a common setup for SSO.

The Path attribute restricts the cookie to URL paths starting with a given prefix. Path=/admin means the cookie only travels with requests to /admin/* URLs.

Set-Cookie: token=xyz; Domain=example.com; Path=/api; Secure; HttpOnly

SameSite — Cross-Site Request Control

The SameSite attribute controls whether cookies are sent with cross-site requests. It is the primary defense against Cross-Site Request Forgery (CSRF) attacks.

ValueBehavior
`Strict`Cookie never sent in cross-site requests — even when following a link
`Lax`Cookie sent with top-level navigations (links, GET forms) but not with subresource loads
`None`Cookie always sent cross-site — requires `Secure` attribute

Lax is the browser default since Chrome 80 (2020). It strikes the right balance: a user clicking a link from an email to your site still sends the session cookie, but a malicious page embedding your API as an <img> tag does not.

Strict breaks any flow where users arrive via external links with session state (e.g., OAuth callbacks, email login links). Use it only for high-security cookies like admin tokens that are never part of a navigation flow.

None is required for cross-site embedded widgets, third-party auth flows, and payment iframes. It must be paired with Secure:

Set-Cookie: embed_token=xyz; SameSite=None; Secure

Secure — HTTPS-Only Transmission

The Secure flag prevents the cookie from being sent over plain HTTP connections. Always set this on any cookie containing session tokens or sensitive data:

# Django settings.py
SESSION_COOKIE_SECURE = True      # Require HTTPS for session cookie
CSRF_COOKIE_SECURE = True         # Require HTTPS for CSRF cookie

Note: Secure does not encrypt the cookie. It only controls which connections the browser will include the cookie with. The cookie value itself travels in plaintext within the TLS tunnel — keep sensitive values server-side and use an opaque session ID in the cookie.

HttpOnly — JavaScript Access Control

HttpOnly prevents JavaScript from reading the cookie via document.cookie. It is the primary defense against Cross-Site Scripting (XSS) cookie theft:

// HttpOnly cookie is NOT accessible here:
document.cookie  // => '' (empty, HttpOnly cookies hidden)

// Regular cookie IS accessible:
document.cookie  // => 'theme=dark; language=en'

Use HttpOnly on all session and authentication cookies. The browser still sends them automatically — your JavaScript just cannot read or modify them.

Max-Age vs Expires — Persistence Control

Session cookies (no expiry) are deleted when the browser closes. Persistent cookies survive restarts:

# Relative: expires in 30 days from now (preferred)
Set-Cookie: pref=dark; Max-Age=2592000

# Absolute: expires at a specific datetime (legacy)
Set-Cookie: pref=dark; Expires=Thu, 01 Jan 2026 00:00:00 GMT

Prefer Max-Age over Expires. It is computed relative to the client clock at the moment of receipt, so it behaves consistently regardless of timezone or clock skew. Setting Max-Age=0 immediately deletes a cookie.

Third-Party Cookies and Privacy

Third-party cookies — those set by a domain other than the one in the browser's address bar — are being phased out by all major browsers due to privacy concerns.

CHIPS: Partitioned Cookies

Cookies Having Independent Partitioned State (CHIPS), also called the Partitioned attribute, is the privacy-preserving replacement. A partitioned cookie is stored separately per top-level site:

Set-Cookie: widget_session=abc; SameSite=None; Secure; Partitioned

With Partitioned, the same cookie set by widget.example.com when embedded on shop.com is isolated from the same cookie when embedded on blog.com. This prevents cross-site tracking while still allowing embedded widgets to maintain state.

Intelligent Tracking Prevention (ITP)

Safari's ITP restricts even same-site cookies set via JavaScript, capping their lifetime at 7 days. If your authentication flow relies on JavaScript-set cookies, switch to server-set HttpOnly cookies — ITP does not restrict those.

Session Management with Cookies

The correct pattern for session management:

  • Generate a cryptographically random session ID (32+ bytes of entropy)
  • Store the session data server-side (database or Redis) keyed by that ID
  • Send only the opaque ID in the cookie
import secrets

def create_session(user_id: int, response: HttpResponse) -> None:
    session_id = secrets.token_urlsafe(32)
    # Store in Redis with TTL
    redis.setex(f'session:{session_id}', 86400, user_id)
    response.set_cookie(
        'session_id',
        session_id,
        max_age=86400,
        secure=True,
        httponly=True,
        samesite='Lax',
    )

Session renewal: Regenerate the session ID after privilege escalation (login, password change) to prevent session fixation attacks.

Browsers enforce a 4KB limit per cookie and typically allow 20-50 cookies per domain. If you hit the limit, requests silently drop the largest cookies. Never store application state in cookies — use them only for IDs that reference server-side storage.

SameSite Breaking POST Forms

If you use SameSite=Strict, any POST from an external page (including email verification links that redirect to a form) will arrive without the session cookie. The user appears logged out. Use Lax for session cookies and Strict only for secondary tokens like CSRF double-submit cookies.

Domain Mismatch in Local Development

Cookies set on localhost are isolated from 127.0.0.1 even though they resolve to the same address. Choose one consistently in development. Also, Secure cookies will not be set on http://localhost in some browsers — use a local HTTPS proxy like mkcert when testing production-like cookie behavior.

Related Protocols

Related Glossary Terms

More in HTTP Fundamentals