Blog / Cookie Security — Secure, HttpOnly, SameSite and the __Host- Prefix, Properly Explained
cookies session-security samesite csrf laravel

Cookie Security — Secure, HttpOnly, SameSite and the __Host- Prefix, Properly Explained

What Secure, HttpOnly and SameSite actually do, the Lax/Strict/None trade-offs and CSRF implications, the __Host- prefix, and Laravel/PHP/Express examples.
The CompleteStatus Team · · 6 min read
Cookie Security — Secure, HttpOnly, SameSite and the __Host- Prefix, Properly Explained
Want this checked continuously?
CompleteStatus grades your headers, SSL and email security 24/7 — free, commercial use allowed.
Monitor your site free

Cookie security comes down to three attributes — Secure, HttpOnly and SameSite — plus one naming prefix, and yet misconfigured session cookies remain one of the most common findings in any web security audit. The stakes are concrete: a session cookie without these flags can be stolen over plain HTTP, exfiltrated by any injected script, or ridden by a cross-site request forgery. Each flag closes exactly one of those doors, and knowing which door is the difference between configuring them correctly and cargo-culting them.

Here's what each attribute actually does, the real trade-offs of SameSite's three modes, and copy-paste settings for PHP, Laravel and Express.

Secure: never send this over plain HTTP

Set-Cookie: session=abc123; Secure

A cookie with Secure is only attached to HTTPS requests. Without it, one request to http://your-site.com — an old link, a typed domain before the redirect — sends the session token in cleartext, where anyone on the network path can read it. Session hijacking via unencrypted cookies predates most people's careers and still works.

There is no trade-off in 2026. Your site runs on HTTPS; every cookie should carry Secure. Pair it with HSTS so the browser won't make that first plaintext request at all.

HttpOnly: invisible to JavaScript

Set-Cookie: session=abc123; Secure; HttpOnly

HttpOnly hides the cookie from document.cookie. If an attacker gets a script running in your page — XSS via a vulnerable dependency, a compromised third-party tag — they cannot simply read the session token and ship it to their server.

Be honest about its limits: an attacker who can run script in your page can still use the session (making authenticated requests from the victim's browser), they just can't steal the token itself for later, sold-on use. HttpOnly raises the bar; your CSP does the heavy lifting against the XSS itself — see our practical CSP guide.

The corollary: if your frontend genuinely needs to read a cookie in JS, that cookie must not be your session token. Session cookies get HttpOnly, always; a separate, non-sensitive cookie can stay readable.

SameSite: the CSRF question

SameSite controls whether the cookie accompanies cross-site requests — requests to your domain initiated from another site. That's precisely the mechanism of CSRF: evil.example submits a hidden form to your-app.com/transfer, and the browser helpfully attaches your session cookie.

Three modes, three trade-offs:

  • SameSite=Strict — the cookie is never sent on cross-site requests, including when a user simply clicks a link to your site from an email or a search result. Maximum CSRF protection, but users following external links arrive logged-out, which is jarring for most apps. Right for high-sensitivity actions (banking flows, admin panels), sometimes deployed as a second "confirmation" cookie alongside a Lax session.
  • SameSite=Lax — the cookie is withheld from cross-site subrequests and form POSTs, but is sent on top-level GET navigations (the user clicking a link). This blocks the classic hidden-form CSRF while keeping external links logged-in. It's the default in Chrome when no attribute is set, and the right default for most session cookies. One caveat: it only helps if your state-changing endpoints refuse GET — a destructive GET route is exempt from Lax's protection by definition.
  • SameSite=None — the cookie is sent on all cross-site requests. Required when your cookie legitimately needs to travel cross-site: embedded widgets, some SSO flows, cross-site XHR with credentials (which also involves CORS). Browsers require Secure alongside None, and reject the pairing without it. None re-opens the CSRF door, so anything using it needs explicit CSRF tokens.

SameSite=Lax is a strong safety net, not a replacement for CSRF tokens: subdomains you don't control, framework quirks and the GET caveat all leave residual risk. Keep your framework's CSRF middleware on.

The __Host- prefix: integrity by naming

A cookie named with the __Host- prefix is only accepted by the browser if it is set with Secure, from an HTTPS page, with Path=/, and without a Domain attribute:

Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/

Why bother? Because ordinary cookies can be planted by related origins: a compromised or malicious subdomain can set a Domain=your-site.com cookie that your main app then trusts — the basis of session-fixation and cookie-tossing attacks. The __Host- rules make that impossible: the cookie provably came from your exact host. The weaker __Secure- prefix only guarantees Secure on HTTPS. If your session cookie doesn't need to be shared across subdomains, __Host- is free hardening.

Session-cookie checklist

  • Secure — always.
  • HttpOnly — always, for anything session- or auth-related.
  • SameSite=Lax — default; Strict for high-sensitivity cookies; None only with a concrete cross-site need and CSRF tokens.
  • __Host- prefix where subdomain sharing isn't needed.
  • No state-changing GET endpoints.
  • Short expiry for the session itself; rotate the token on login and privilege change.

Framework examples

PHP (native sessions) — in php.ini or before session_start():

session_set_cookie_params([
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
    'path'     => '/',
]);
session_start();

Laravelconfig/session.php (backed by .env):

'secure'    => env('SESSION_SECURE_COOKIE', true),
'http_only' => env('SESSION_HTTP_ONLY', true),
'same_site' => env('SESSION_SAME_SITE', 'lax'),

Laravel encrypts its cookies and ships CSRF middleware by default — leave both on; the flags above are what makes the session cookie itself transport-safe.

Express (express-session):

app.set('trust proxy', 1); // if behind a reverse proxy
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    secure: true,
    httpOnly: true,
    sameSite: 'lax',
  },
}));

The trust proxy line matters: behind nginx or a load balancer, Express otherwise sees plain HTTP and secure: true cookies never get set — a classic "works locally, breaks in prod" trap.

Cookies are part of your security grade

Cookie flags aren't a set-and-forget: a framework upgrade, a new auth library, or a hastily added cookie can quietly ship without Secure or HttpOnly, and nothing will error. They're one of the checks behind CompleteStatus's continuous A+–F security grading, alongside the headers covered in our complete security-headers guide — with the exact fix attached to every finding, and an alert the moment your grade drops.

See your full grade — headers, cookie flags and more — with the free security score checker, then start monitoring free to catch regressions before an auditor (or an attacker) does. The free tier allows commercial use.

Stop checking by hand

CompleteStatus runs these exact checks around the clock — uptime, SSL, DNS, security headers and SPF/DKIM/DMARC — and alerts you the moment something changes. One dashboard, one bill.
Start free Run a free check
Uptime is table stakes. We watch the rest — security headers, email authentication, certs and DNS, with the fix attached.
Start free
Company
© 2026 CompleteStatus. All rights reserved. CompleteStatus — operated in the United States · support@completestatus.com