Web

HTTP Cookies and Sessions: The Attributes That Decide Security

A cookie is a tiny key-value pair with five security attributes that decide whether your auth is hardened or hilariously broken. Learn each attribute, the SameSite changes, and the modern session-cookie patterns.

11 min read

Cookies have been around since 1994, and most web developers still get them wrong. The five security attributes (HttpOnly, Secure, SameSite, Domain, Path) determine whether your auth is rock-solid or comically vulnerable. Browser changes around SameSite shifted the defaults — what worked five years ago is broken now.

The cookie format

Server sets a cookie via Set-Cookie header:

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600

Browser sends matching cookies on subsequent requests via Cookie header:

Cookie: session_id=abc123; theme=dark

The five attributes that matter for security

1. HttpOnly

Prevents JavaScript from accessing the cookie via document.cookie. Critical for session cookies — XSS attacks can't steal HttpOnly cookies.

Use HttpOnly for: session IDs, JWT tokens, anything authentication-related.

Don't use HttpOnly for: cookies your JavaScript actually needs (theme preferences, UI state).

2. Secure

Cookie is only sent over HTTPS. Without this, a man-in-the-middle on an HTTP page could read the cookie.

Modern best practice: always set Secure. Even cookies that don't seem sensitive can leak fingerprinting data if intercepted.

3. SameSite

Controls whether the cookie is sent on cross-site requests:

  • Strict: never sent on cross-site requests. Most secure but breaks login flows that involve external redirects (OAuth, magic links).
  • Lax (default in modern browsers): sent on top-level navigations (clicking a link to your site) but not on cross-origin sub-requests (image, iframe, fetch from another site).
  • None: sent on all requests. Required for cross-site cookies but must be paired with Secure.

The SameSite default change

Chrome 80+ (early 2020) and most browsers now default to SameSite=Lax if not specified. Apps that previously worked "by accident" relying on the old default (None) broke when the change rolled out. If you're embedding your auth flow in an iframe or making cross-site XHR with credentials, you must explicitly set SameSite=None; Secure.

4. Domain

Specifies which hosts can receive the cookie:

  • Not set: cookie sent only to the exact host that set it.
  • Domain=example.com: cookie sent to example.com and all subdomains (api.example.com, www.example.com).
  • Cannot set Domain=.com or other public suffixes: browser rejects.

For session cookies, prefer host-only (don't set Domain) unless you specifically need cross-subdomain auth.

5. Path

Limits the cookie to a path prefix. Path=/admin means cookie only sent for URLs starting with /admin. Useful for namespacing but not a security primitive — JavaScript on/admin can read cookies set for / regardless.

Lifetime attributes

  • Max-Age: seconds until the cookie expires. Max-Age=3600 = 1 hour.
  • Expires: absolute timestamp. Expires=Wed, 21 Oct 2026 07:28:00 GMT
  • Neither: session cookie. Deleted when browser closes.

If both are set, Max-Age wins. Prefer Max-Age — Expires has been the source of many bugs around server clock skew.

The CSRF attack and how cookies enable it

Cookie-based auth has a weakness: cookies are sent automatically by the browser. An attacker can trick a user into making a state-changing request to your site:

<!-- on 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>

If the user is logged into bank.com, the browser sends their session cookie, and the request succeeds. This is CSRF (Cross-Site Request Forgery).

Modern CSRF defenses

  1. SameSite=Lax (or Strict). Browser doesn't send the cookie on cross-site POSTs. Default in most modern browsers — but verify your auth cookie has it explicitly set.
  2. CSRF tokens. Server includes a unique token in every form/page; submissions must include it. Token isn't in cookies, so attacker's site can't read it (Same-Origin Policy).
  3. Custom headers. Require a custom header (e.g., X-Requested-With) for state-changing requests. Cross-site requests can't set arbitrary headers without preflight, which fails for forms.
  4. Re-authentication for sensitive actions. Password change, payment, etc., require recent re-auth.

Cookies vs Authorization header

For browser apps, cookies are usually better than Authorization headers because:

  • HttpOnly prevents XSS theft.
  • Browser handles them automatically.
  • Cleared when browser closes (session cookies).

For mobile apps and APIs, Authorization headers (with bearer tokens) are usually better:

  • No CSRF risk (only sent when explicitly added by client code).
  • More portable across origins.
  • Doesn't require cookie-handling code on the client.

The session cookie pattern

Most secure pattern for browser auth:

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

Server stores the session ID in a database (Redis, PostgreSQL) mapping to user data. To revoke a session, delete the server-side record — instant invalidation without waiting for cookie expiration.

For OAuth/SSO scenarios crossing domains, you may need:

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=None; Path=/; Max-Age=86400

But understand that SameSite=None opens you to CSRF unless you have additional protection (CSRF tokens, custom headers).

Cookie size limits

  • Each cookie: max 4KB (name + value + attributes).
  • Per origin: typically 50 cookies max.
  • Total per browser: limits vary, ~3000.

Don't store user data in cookies. Use a session ID and store data server-side. The 4KB limit sneaks up on apps with multiple frameworks each setting their own cookies.

Common cookie mistakes

  • Session cookie without HttpOnly. XSS gets your auth.
  • No Secure flag in production. Cookie leaks on HTTP.
  • SameSite=None without Secure. Browser rejects the cookie entirely.
  • Storing sensitive data in client-readable cookies. Tokens, PII, secrets — never client-readable.
  • Forgetting to set Domain consistently across subdomains. Causes inconsistent auth on www vs non-www.
  • Setting both Max-Age and Expires. Max-Age wins; Expires is ignored. Just use one.
  • Cookie name collisions. Multiple frameworks setting session or auth. Namespace with prefixes.

Key Takeaways

  • Five security attributes: HttpOnly, Secure, SameSite, Domain, Path. Always set all five for session cookies.
  • SameSite=Lax is the modern default. Apps relying on old None-by-default behavior broke in 2020 when browsers tightened.
  • CSRF defense modernizes: SameSite cookies + CSRF tokens + custom headers for any state-changing action.
  • Cookies for browser apps; Authorization headers for mobile and APIs. Each fits different threat models.
  • 4KB cookie limit is real. Store session IDs in cookies; store data server-side. Don't pile state into cookies.