What this means
The finding means a cookie that carries a session or token is missing at least one protective attribute. Secure stops the cookie being sent over plain http. HttpOnly hides it from JavaScript, so a cross-site scripting bug cannot read it. SameSite limits when it is sent on requests coming from other sites, which reduces cross-site request forgery. Cookies for harmless preferences, such as a theme, do not need HttpOnly.
Why it happens
- The cookie is set from browser code with document.cookie, which cannot set HttpOnly.
- Generated server code calls a set-cookie helper with only a name and value, so every attribute falls back to the default.
- Secure was switched off so login would work on http://localhost and the change was shipped.
- SameSite=None was copied from a snippet. It is needed only for genuine cross-site use and must be paired with Secure.
- A proxy terminates HTTPS and the framework, unaware the connection is secure, drops the Secure flag. In Express this is fixed with the trust proxy setting.
How to fix it
- 1Open devtools, Application, Cookies and find which cookies lack the flags. Note which ones hold sessions or tokens.
- 2Find where each is set: your code, an auth library's config, or a third-party service.
- 3Set the cookie on the server with httpOnly, secure and sameSite. Keep secure on in production. Browsers treat localhost as secure, so you rarely need to turn it off for development.
- 4Pick SameSite=Lax for most apps. Use Strict if no flow depends on arriving from another site already logged in.
- 5Consider the __Host- name prefix, which makes browsers require Secure, Path=/ and no Domain attribute.
- 6If the token currently lives in localStorage or a readable cookie, move it to an HttpOnly cookie set by your server.
import { cookies } from 'next/headers';
export async function POST() {
const cookieStore = await cookies();
cookieStore.set('__Host-session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
});
return Response.json({ ok: true });
}
app.set('trust proxy', 1);
res.cookie('__Host-session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 1000 * 60 * 60 * 24 * 7,
});How to confirm the fix
- Log in to your own app, then check devtools, Application, Cookies. The session cookie should show ticks under HttpOnly and Secure, and Lax or Strict under SameSite.
- In the console, run document.cookie. The session cookie must not appear in the output.
- Inspect the raw header from your login endpoint with the curl command below, using a test account.
curl -s -D - -o /dev/null https://www.yourapp.com/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"test@yourapp.com","password":"YOUR_TEST_PASSWORD"}' | grep -i set-cookieFrequently asked questions
Does HttpOnly stop cross-site scripting?
No. It stops injected script from reading the cookie. The script can still act as the user while the page is open. Fix the injection and keep HttpOnly as a limit on the damage.
Browsers default to SameSite=Lax now. Do I still need to set it?
Set it explicitly. Defaults differ between browsers, and an explicit value documents your intent and survives library changes.
A third-party cookie on my site lacks the flags. Can I fix it?
Only the service that sets it can. Check whether the provider has a setting for it, and focus first on cookies that carry your own sessions.
My auth library stores the token in localStorage. Is that a finding too?
It is a related risk: anything in localStorage is readable by any script on the page. Where the library supports a server-side cookie mode, prefer it.