Secure, HttpOnly, SameSite: Session Cookie Flags Explained
Three attributes on one Set-Cookie header decide whether a stolen network connection, an injected script or a malicious link can take over your users’ sessions. What each flag does and the value to use.
By UseShipReady
A session cookie should carry three flags. Secure means it is only sent over HTTPS, so it cannot be read off an unencrypted connection. HttpOnly means JavaScript cannot read it, so an injected script cannot steal it. SameSite=Lax (or Strict) means other sites cannot make the browser send it along with their requests, which blocks most cross-site request forgery. A safe default is Set-Cookie: session=...; Path=/; Secure; HttpOnly; SameSite=Lax.
Your session cookie is the thing that says "this browser is signed in as this user". Whoever holds it is that user, with no password needed. The flags on the cookie decide which of the common ways of stealing or misusing it still work. They are one line of configuration, and they are exactly the line AI-generated auth code most often gets wrong or leaves out.
What does the Secure flag do?
Secure tells the browser to send the cookie only over HTTPS. Without it, any request your site makes over plain HTTP, such as a visitor typing your domain without https:// before the redirect kicks in, carries the cookie in clear text. Anyone on the same café Wi-Fi can read it and replay it. With it, the cookie never leaves the browser unencrypted.
The flag works best alongside HSTS, which stops the unencrypted first request from happening at all. Both are on the security headers checklist.
What does the HttpOnly flag do?
HttpOnly hides the cookie from JavaScript: document.cookie does not include it. It is still sent with every request, so your server sees it normally. What changes is the outcome of a cross-site scripting bug. Without HttpOnly, one injected script can read the session and send it to an attacker, who can then use it from anywhere, for as long as the session lives. With HttpOnly, the script can still misuse the page while it is open, but it cannot take the session away with it.
Some auth libraries deliberately set cookies JavaScript can read, because their browser client needs the token. That is a documented trade-off, not automatically a bug, but it means the cookie’s safety rests entirely on your site having no script injection, so your Content-Security-Policy matters more.
What does SameSite do, and which value should I use?
SameSite controls whether the cookie is attached to requests that another site starts. Without it, a malicious page could submit a hidden form to your "change email" endpoint, and the browser would helpfully attach your user’s session cookie. That is cross-site request forgery.
| Value | Sent on cross-site requests? | Use it when |
|---|---|---|
Strict | Never | High-risk apps where users always arrive by typing the URL or a bookmark |
Lax | Only on top-level navigation with a safe method (a normal link click) | Almost every session cookie. The right default |
None | Always; requires Secure | Only when your app genuinely runs inside another site, such as an embedded widget |
Strict has a surprising side effect: a user clicking a link to your site from an email arrives looking signed out, because the cookie is withheld on that first navigation. That is why Lax is the usual choice. Chrome treats cookies with no SameSite attribute as Lax, but other browsers have differed, so set it explicitly rather than relying on a default.
SameSite=None without Secure
Modern browsers reject a cookie that says SameSite=None without Secure: they do not store it at all. The symptom is a login that works locally and silently fails in production, or works in one browser and not another. If you need None, add Secure, and ask whether you really need None.
How do I check the flags on my session cookie?
# Sign-in usually sets the cookie; check the response headers of that request
curl -sI https://yourdomain.com/login | grep -i set-cookieOr open DevTools, go to Application, then Cookies, and look at the Secure, HttpOnly and SameSite columns for your session cookie. Check after signing in: a logged-out homepage often sets no session cookie, so a clean result there proves little.
How do I set secure session cookies?
// Next.js (App Router), in a route handler or server action
import { cookies } from 'next/headers'
(await cookies()).set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7, // one week
})
// Express
res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })Two more attributes are worth knowing. Naming the cookie with the __Host- prefix (for example __Host-session) makes the browser enforce Secure, Path=/ and no Domain attribute, which stops a compromised subdomain from overwriting it. And a Max-Age sets how long a stolen cookie stays useful, so keep it no longer than your product needs.
How long should a session cookie last?
As short as your users will tolerate. A cookie with no Max-Age or Expires is a session cookie in the browser’s sense and is cleared when the browser fully closes, although modern browsers that restore tabs often restore these cookies too. For "remember me" logins, days or a few weeks is typical for consumer apps; admin panels and anything touching money deserve hours. Whatever the cookie says, the server should also expire the session on its side, and invalidate it on sign-out and password change, so that a copied cookie stops working even if the browser would keep sending it.
Can a CDN leak session cookies between users?
Yes, and it is one of the worst ways a session can leak. If a response that sets a session cookie is marked as cacheable by shared caches, a CDN can store it, including the Set-Cookie header, and serve it to the next visitor, who is then signed in as the first one. Any response that sets a session cookie should carry Cache-Control: private or no-store. UseShipReady rates a session cookie in a cacheable response high for this reason.
How UseShipReady checks session cookies
UseShipReady reads the Set-Cookie headers on every page it crawls, including sign-in routes, and examines cookies whose names mark them as sessions, such as session, token, auth or sid. CSRF tokens, which are meant to be readable by JavaScript, are deliberately left out. Each missing flag is reported separately: no Secure flag, no HttpOnly flag, no SameSite attribute and SameSite=None without Secure.
Cookies set by JavaScript through document.cookie are not visible in response headers, and they cannot be HttpOnly anyway. If your session lives in one, that is a design question for your auth setup rather than a flag to add. The stack security guides cover what each AI builder tends to generate for auth by default.
Frequently asked questions
- Should every cookie be HttpOnly?
- Every cookie that holds a session or authentication token should be. Cookies that your own JavaScript genuinely needs to read, such as a theme preference or a CSRF token used in the double-submit pattern, cannot be HttpOnly, and that is fine because they are not credentials.
- Does SameSite=Lax fully protect against CSRF?
- It blocks most cross-site request forgery, because the cookie is not sent on cross-site form posts or background requests. It does not cover state-changing GET requests, which is one more reason never to change data on GET. For sensitive actions, a CSRF token adds a second layer.
- Why is my cookie not being set in production?
- A common cause is SameSite=None without Secure, which modern browsers reject. Another is setting Secure on a site served over plain HTTP. Check the browser console and the Application tab, which usually say why a cookie was blocked.
- Is it safer to store the session token in localStorage?
- No. Anything in localStorage is readable by any script on the page, so a single injected script can steal it. An HttpOnly cookie keeps the token out of JavaScript’s reach entirely.