Skip to content

Session cookie flags

Severity: highApplies to: express-session 1.xFacts last verified 2026-07-26 against express-session 1.19.0

From express-session’s documentation: “By default, the Secure attribute is not set.” The package cannot know whether you are serving over HTTPS, so it declines to guess — which means the flag is yours to set, and nothing warns if you never do.

the fix
app.set("trust proxy", 1); // see the note below — this is not optional
app.use(
session({
// ...
cookie: {
secure: true, // HTTPS only
httpOnly: true, // not readable from JavaScript
sameSite: "lax", // sent on top-level navigation, not cross-site POSTs
maxAge: 1000 * 60 * 60 * 8,
},
})
);
verify it workedRun this in: http response
Terminal window
curl -sI https://example.com/login | grep -i set-cookie

Look for Secure; HttpOnly; SameSite=Lax on the real response. Reading the config proves nothing here, because the most common failure produces a config that says secure: true and a response with no cookie at all.

Why secure: true alone can log everyone out

Section titled “Why secure: true alone can log everyone out”

This is the trap, and express-session’s docs call it out directly:

If secure is set, and you access your site over HTTP, the cookie will not be set. If you have your node.js behind a proxy and are using secure: true, you need to set “trust proxy” in express.

Behind a TLS-terminating proxy, Express receives plain HTTP. With secure: true and trust proxy unset, express-session decides the connection is insecure and declines to send the cookie at all. There is no error. Login appears to succeed and the user is immediately anonymous again.

app.set("trust proxy", 1) tells Express to believe X-Forwarded-Proto from the first hop. It is the same one-line-either-side problem as Django’s SECURE_PROXY_SSL_HEADER, and it comes with the same caveat — the proxy must strip that header from client requests, or anyone can assert it. That is on trust proxy.

before you ship this

secure: true breaks local development over http://localhost for exactly the reason above. Gate it on the environment rather than hardcoding it:

const isProd = process.env.NODE_ENV === "production";
cookie: { secure: isProd, httpOnly: true, sameSite: "lax" }

sameSite: "strict" withholds the cookie even on a top-level navigation from another site, so a user following a link from an email arrives logged out. lax is the right default for almost every application.