cookies · July 10, 2026 · 7 min read
Cookie Security: Secure, HttpOnly, and SameSite Explained
What Secure, HttpOnly, and SameSite actually do, why session cookies need them, and how to set cookie flags correctly in any framework.
Whatever your auth provider’s landing page says, the end result of logging in is almost always a cookie. That cookie is the login — whoever holds it is you, no password required. A handful of attributes on one Set-Cookie line decide how hard that cookie is to steal or misuse.
Cookies still run the web’s authentication
Sessions, JWTs stored in cookies, OAuth callbacks — most of it lands in a cookie the browser attaches to every matching request. The attributes on that cookie are handling instructions to the browser: where this cookie may travel, who may read it, when it should be sent. Browsers enforce them exactly, which makes them cheap and reliable protection — but only if they are set. Automated scanners read cookie attributes on every site they touch, so a permissive session cookie does not stay unnoticed.
Each attribute below closes a different door. None of them is optional for a session cookie.
Secure: never over plain HTTP
Without Secure, the browser will attach your cookie to any http:// request for your domain — where anyone on the network path (public Wi-Fi, a compromised router) can read it. Even a fully-HTTPS site can trip here: one hardcoded http:// link or a user typing the bare domain is enough to send the cookie in cleartext once, and once is enough.
There is no downside on a modern site. Set Secure on every cookie you issue.
HttpOnly: JavaScript can’t read it
HttpOnly makes a cookie invisible to document.cookie. The browser still sends it with requests; scripts simply cannot read it.
Why that matters: if your site ever has an XSS bug — and you should plan for the possibility — injected JavaScript can read anything your own scripts can read. With HttpOnly set, the session doesn’t walk out the door with it. The flag does not fix the XSS; it shrinks the blast radius. Pair it with a Content-Security-Policy, which attacks the same problem from the other side — see the security headers guide.
Set it on every session and auth cookie. Skip it only for cookies your frontend genuinely reads, like a theme preference.
SameSite: CSRF protection built into the browser
A cross-site request is one your browser makes to your site while the user is on someone else’s site — and historically cookies went along for the ride, letting a malicious page trigger authenticated actions. SameSite controls that:
Lax— sent when the user navigates to your site (clicking a link), but not on cross-site form posts or embedded subresources. The right default for most session cookies. Modern browsers often apply it when you specify nothing, but say it explicitly.Strict— never sent on any cross-site request, even a clicked link. Stronger, with a UX cost: a user arriving from an email link looks logged out until they navigate again. Good for admin panels and high-stakes apps.None— sent on all cross-site requests. Only needed when your cookie must work inside another site’s context, such as an embedded widget or some SSO flows. Browsers requireSecurealongsideNone;SameSite=NonewithoutSecureis simply rejected, a surprisingly common misconfiguration.
If your frontend and API live on different origins, SameSite interacts with your CORS setup — see CORS misconfiguration explained.
__Host- and __Secure-: guarantees in the name
Cookie prefixes are contracts the browser enforces at set-time. A cookie named __Secure-* must be set with Secure over HTTPS, or the browser refuses to store it. __Host-* goes further: it also requires Path=/ and forbids the Domain attribute, pinning the cookie to exactly one host — so nothing set from a subdomain can shadow or overwrite it.
Renaming your session cookie to __Host-session costs one line of config and buys guarantees the attributes alone cannot make. Browsers that predate prefixes just see an odd name, so there is no downside to adopting them.
Domain scope: the quiet leak
Set-Cookie: session=...; Domain=.example.com sends that session cookie to every subdomain you have — staging, the old marketing microsite, the vendor tool living on a subdomain. If any one of them is compromised, the session leaks with it.
The fix is to do less: omit the Domain attribute entirely and the cookie becomes host-only, sent solely to the host that set it. Broad scope is occasionally necessary for true cross-subdomain SSO; for an ordinary app it is unnecessary surface.
Which cookies matter most
Triage by what the cookie unlocks:
- Session, auth, and token cookies: the full treatment —
Secure,HttpOnly,SameSite=LaxorStrict, host-only scope, ideally a__Host-prefix. - Preferences and analytics:
Secureand a saneSameSite; addHttpOnlywherever JavaScript does not need to read them.
The line worth memorizing:
Set-Cookie: __Host-session=<value>; Path=/; Secure; HttpOnly; SameSite=LaxHow to check your cookies
Open DevTools → Application tab → Cookies (Storage tab in Firefox). Every cookie is listed with checkbox columns for Secure and HttpOnly and a SameSite column. Log in first, then look at the session cookie specifically — blank SameSite and missing checkmarks there are findings, not cosmetics.
How to fix it in your framework
Express with express-session:
app.use(session({
name: '__Host-session',
cookie: { secure: true, httpOnly: true, sameSite: 'lax', path: '/' },
}));Next.js route handler:
cookies().set('__Host-session', token, {
secure: true, httpOnly: true, sameSite: 'lax', path: '/',
});Django: SESSION_COOKIE_SECURE = True, SESSION_COOKIE_HTTPONLY = True, SESSION_COOKIE_SAMESITE = "Lax". Rails and Laravel have equivalent one-line session settings.
If an AI tool wrote your auth — Cursor, Lovable, Bolt, v0 — check what it actually set. Generated session code works identically with or without these flags, so nothing forces the model to include them. More patterns like this in the vibe-coder’s guide.
How BoringSec checks this
BoringSec’s cookie scanner analyzes every cookie your site sets and grades the attributes. A missing Secure flag on an HTTPS site is a high-severity finding, and so is a session-like cookie without HttpOnly — the flag that keeps an XSS bug from turning into session theft. The scanner flags SameSite problems including the None-without-Secure trap, verifies __Host-/__Secure- prefix compliance, and calls out overly broad Domain scope on session cookies.
One design note: the scanner never stores your cookie values — only observations about their attributes. Every finding cites its standard (OWASP, MDN, RFC), and per our evidence-first rule, critical and high findings carry captured proof or get downgraded to medium. Details in the methodology.
FAQ
Should every cookie be HttpOnly?
What is SameSite=None actually for?
Secure with it; SameSite=None without Secure is rejected outright.Do __Host- and __Secure- prefixes work in all browsers?
Keep reading
Check your site now
One scan applies the relevant checks from 20 base URL modules plus 3 verified-owner background engines. Free, no signup, with base results appearing first.
Scan your site free