Secure sessions and cookies: HttpOnly, Secure, SameSite and session fixation

After login, a session cookie is the key to the account. Set it up carefully and a stolen or forged cookie loses most of its power.

A session cookie protected by security flags

HTTP is stateless: the server does not remember you between requests. After you log in, the application gives your browser a session identifier, usually in a cookie, that the browser sends with every request. Whoever holds that identifier is treated as you. So protecting it is as important as protecting the password.

This is a defensive guide to setting up sessions and cookies safely.

How a session works

  1. You log in with valid credentials.
  2. The server creates a session record (user, creation time, expiry) and a long, random session ID.
  3. It sends the ID in a Set-Cookie header.
  4. The browser sends the cookie back on each request, and the server looks up the session.
  5. On logout or expiry the server destroys the session.
Set-Cookie: sid=9f3a...random...c21; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600

The session ID itself

  • Generate it with a cryptographically secure random generator, with enough entropy (at least 128 bits is a common guideline). Never use predictable values like counters, timestamps or user IDs.
  • Treat it like a password. Do not put it in URLs (they end up in logs, history and referrer headers).
  • Store session data on the server (database or cache such as Redis) and keep only the ID in the cookie, or use properly protected, signed and encrypted cookie sessions provided by a mature framework.
  • Use your framework's built-in session management instead of building your own.

Secure: the browser sends the cookie only over HTTPS. Always set it in production, so it is not exposed on plain HTTP connections.

HttpOnly: JavaScript cannot read the cookie through document.cookie. This limits the damage of cross-site scripting (XSS), because a malicious script cannot simply steal the session ID. (XSS can still perform actions as the user inside the page, so fix XSS too.)

SameSite: controls whether the cookie is sent on cross-site requests, which reduces CSRF risk.

  • Strict: sent only for same-site requests. Strongest, but a link from another site arrives without the cookie, so the user may look logged out on the first click.
  • Lax: sent for same-site requests and for top-level navigations with safe methods (such as clicking a link). A good default, and the default in current major browsers when not specified.
  • None: sent on all cross-site requests and requires Secure. Use only when you truly need cross-site cookies, such as embedded third-party content.

Domain and Path: keep them as narrow as possible. Omitting Domain ties the cookie to the exact host and not its subdomains.

Max-Age / Expires: limits lifetime (see expiry below). Without them it is a session cookie that ends when the browser closes, although some browsers restore sessions.

Cookie prefixes: __Host- (requires Secure, Path=/, and no Domain) and __Secure- give extra protection against some overwrite attacks. For example: __Host-sid.

Important: SameSite helps with CSRF but is not a complete defence. Keep CSRF tokens or equivalent protections for state-changing requests.

Session fixation

Session fixation happens when an attacker makes a victim use a session ID the attacker already knows (for example by planting it before login). If the app keeps the same ID after login, the attacker is now logged in as the victim.

Defence: issue a new session ID whenever the privilege level changes, especially at login, and also on password change or role change. Invalidate the old ID.

Expiry and logout

  • Idle timeout: end sessions after a period of inactivity (shorter for sensitive apps, such as banking).
  • Absolute timeout: end sessions after a maximum lifetime, even if active.
  • "Remember me": use a separate, longer-lived, revocable token, and require re-authentication for sensitive actions.
  • Logout: destroy the session on the server and clear the cookie. Just deleting the browser cookie is not enough if the server still accepts the old ID.
  • Password change or reset: invalidate all other sessions.
  • Let users see and end active sessions on other devices where appropriate.

Re-authentication for sensitive actions

For changing email or password, adding payment methods or deleting an account, ask for the password again (or a second factor). This limits what a hijacked session can do.

Detecting and limiting abuse

  • Bind session records to some context, but be careful: tying a session strictly to an IP address can break for mobile users. Use it as a risk signal (alert or step-up check) rather than a hard rule.
  • Log logins, logouts and suspicious changes, and alert on unusual patterns.
  • Rate limit authentication endpoints.
  • Use multi-factor authentication to make a stolen password insufficient.

Server-side sessions vs JWTs in the browser

Many tutorials store a JWT in localStorage. That has security trade-offs:

  • Anything in localStorage is readable by any script on your page, so an XSS bug can steal it.
  • Stateless JWTs are hard to revoke before they expire.

For a typical web app, server-side sessions or an HttpOnly Secure SameSite cookie are usually the safer default. If you do use JWTs, keep them short-lived, store refresh tokens carefully (in an HttpOnly cookie), validate every claim, and have a revocation plan.

Other protections that support sessions

  • HTTPS everywhere, plus the Strict-Transport-Security header.
  • A strong Content Security Policy and output encoding to reduce XSS.
  • Anti-CSRF tokens for state-changing requests.
  • Avoid putting sensitive information into cookies that the client can read or edit. If you must store data in a cookie, sign and encrypt it.
  • Keep the framework and its session library up to date.

Quick checklist

  1. Random, long session IDs from a secure generator.
  2. Cookie flags: Secure, HttpOnly, SameSite=Lax (or Strict), narrow scope, __Host- prefix where possible.
  3. New session ID at login and privilege changes.
  4. Idle and absolute timeouts, and server-side logout.
  5. Invalidate sessions after password change.
  6. Re-authenticate for sensitive actions.
  7. CSRF protection and XSS defences in addition to cookie flags.
  8. Never put session IDs in URLs or logs.

Test your own site

In the browser developer tools (Application or Storage tab) inspect your cookies and check the flags. Also verify that after logging out, the old session ID no longer works. Test only applications you own or are authorised to test.

Takeaway: treat the session cookie like a password. Use random IDs, set Secure, HttpOnly and SameSite, rotate the ID at login, expire and destroy sessions on the server, and combine it with CSRF and XSS protections.


Web security series, Part 3 (accounts and defence): 3 of 5. Previous: OAuth 2.0 and OpenID Connect explained, and how to implement them securely.

Keep reading