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.
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=3600Browser sends matching cookies on subsequent requests via Cookie header:
Cookie: session_id=abc123; theme=darkThe 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
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=.comor 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
- 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.
- 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).
- 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. - 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=86400Server 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=86400But 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
sessionorauth. 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.