A cookie is just a small string your server asks the browser to store and send back on every request. When that cookie holds a login session, it is the user's identity — so how the cookie is configured decides how easily an attacker can steal that identity. Three attributes do almost all of the work: Secure, HttpOnly and SameSite. This guide explains what each one does, the prefixes and scoping rules that harden them further, and how to set a session cookie correctly.
Each flag has its own focused guide if you want to go straight to one:
| Flag | What it does |
|---|---|
| The Secure flag | Sends the cookie only over HTTPS, so a session token is never exposed in the clear. |
| The HttpOnly flag | Hides the cookie from JavaScript, so a cross-site scripting bug can't steal the session. |
| The SameSite attribute | Controls cross-site sending to defend against CSRF; a missing value is only a minor gap. |
Why session cookies are a security boundary
HTTP is stateless: the server forgets you between requests. A session cookie bridges that gap — after you log in, the server hands the browser a token and trusts whoever presents that token on the next request. That convenience is also the risk. Anyone who obtains the cookie can act as you without ever knowing your password, and most account-takeover attacks target the cookie rather than the credentials.
There are three realistic ways a session cookie leaks: it travels over plain HTTP and is read on the network, JavaScript injected through a cross-site scripting (XSS) bug reads it, or a malicious site rides your logged-in session through a cross-site request (CSRF). Each of the three flags closes one of those doors.
The three flags that matter
Secure — never send the cookie in the clear
The Secure flag tells the browser to send the cookie only over HTTPS. Without it, a single plain-HTTP request — a typed URL, an old bookmark, a downgrade attack — puts the session token on the wire where anyone on the network path can read it. On an HTTPS site every cookie should be Secure; there is no downside. Full guide to the Secure flag →
HttpOnly — keep the cookie away from JavaScript
HttpOnly hides the cookie from document.cookie, so page JavaScript cannot read it. This is what turns a cross-site scripting bug from "attacker runs script" into "attacker runs script but still can't steal the session." A login or session cookie should always be HttpOnly. The only cookies that should omit it are ones the front-end genuinely must read, such as a CSRF double-submit token. Full guide to HttpOnly →
SameSite — control cross-site sending
SameSite decides whether the cookie is attached to requests that originate from other sites. Strict never sends it cross-site; Lax sends it only on top-level navigations; None always sends it (and then Secure is mandatory). This is the primary defence against CSRF. Crucially, modern browsers now default to Lax when the attribute is absent, so a missing SameSite is a minor hardening gap rather than an open vulnerability. Full guide to SameSite →
Secure or HttpOnly is genuinely exploitable and deserves urgency. A missing SameSite is informational because the browser default already covers the common case. Tools that flag all three as the same "medium" are crying wolf.Cookie name prefixes: __Host- and __Secure-
Two special name prefixes let the browser enforce extra guarantees, so a misconfiguration can't silently weaken the cookie:
__Secure-— the browser will only accept the cookie if it carries theSecureflag and was set over HTTPS.__Host-— the strongest option: the cookie must beSecure, set withPath=/, and have noDomainattribute, which locks it to the exact host that set it (no subdomains). Ideal for session cookies.
If a cookie claims one of these prefixes but breaks the rules, the browser rejects it — so a prefix violation is a real bug, not a style nit.
Don't scope session cookies too broadly
The Domain attribute controls which hosts receive the cookie. Setting Domain=example.com on a session cookie shares it with every subdomain —blog.example.com, status.example.com, a marketing microsite — so a vulnerability on any one of them can expose the session for all of them. Unless you genuinely need cross-subdomain sessions, omit Domain (or use the __Host- prefix) so the cookie stays bound to the single host that issued it.
What a correct session cookie looks like
The safe baseline for a session/auth cookie on an HTTPS site is all three flags together:
Set-Cookie: __Host-sid=<token>; Secure; HttpOnly; SameSite=Lax; Path=/That cookie is sent only over HTTPS, is invisible to JavaScript, is not attached to cross-site requests, and — thanks to the __Host- prefix — is locked to the exact host with no Domain widening. Use SameSite=Strict if your app never relies on following an inbound link into an authenticated page; use None; Secure only for cookies that must work in a genuine cross-site context (for example an embedded widget).
Express, Django and Rails all expose these as a one-line cookie option:
// Express (cookie-session / express-session)
res.cookie("sid", token, { secure: true, httpOnly: true, sameSite: "lax", path: "/" });
# Django settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
# Rails config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: "_app_session", secure: true, httponly: true, same_site: :laxWant to see how your site's cookies are configured right now?
Run the free cookie checker →What a passive check can and can't see
A public, unauthenticated checker reads the Set-Cookie headers your site returns to an anonymous visitor — the homepage plus any redirect hops. That covers the cookies most sites set before login, but cookies issued after you sign in are not visible to a passive check. Treat a clean public result as "the anonymous surface is healthy" and review your post-login cookies in the same way during development.
For the individual flags, read the focused guides: Secure, HttpOnly and SameSite. To see your cookies graded in seconds, use the cookie security checker.