Secure Cookies & the __Host- Prefix
httpOnly, secure, SameSite and the __Host- prefix turn a cookie from a liability into a hardened credential store.
If you store any session id or token in a cookie, its flags are your security posture. Learn the four that matter and one prefix that quietly enforces them.
| Flag | What it does |
|---|---|
httpOnly | JavaScript cannot read it — blocks XSS token theft. |
secure | Sent only over HTTPS — no plaintext leak. |
sameSite | 'lax'/'strict' withholds it cross-site — CSRF defense. |
signed | Tamper-evident via cookie-parser secret. |
The __Host- prefix
Name a cookie with the __Host- prefix and browsers refuse to accept it unless it is secure, has path=/, and has no domain. It cryptographically pins the cookie to your exact origin, defeating subdomain-injection and cookie-fixation attacks — a free, enforced-by-the-browser guarantee.
res.cookie('__Host-sid', id, {
httpOnly: true, secure: true, sameSite: 'strict', path: '/',
});Example
import express from 'express';
import session from 'express-session';
import { RedisStore } from 'connect-redis';
const app = express();
app.set('trust proxy', 1); // required so `secure` cookies work behind a proxy
app.use(
session({
store: new RedisStore({ client: redisClient }), // never the default MemoryStore in prod
name: '__Host-sid', // browser-enforced hardening
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
rolling: true, // refresh maxAge on activity
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/', // required by __Host-
maxAge: 1000 * 60 * 30, // 30 min, matches session TTL
},
}),
);
app.post('/login', (req, res) => {
// Regenerate the session id on privilege change to prevent fixation.
req.session.regenerate((err) => {
if (err) return res.status(500).end();
req.session.userId = 42;
res.json({ ok: true });
});
});When to use it
- A production login sets a Secure, HttpOnly, SameSite=Strict cookie so it is never exposed to JavaScript and only sent over HTTPS.
- A developer uses cookie-parser with a signing secret so the app can detect if a session cookie has been tampered with in transit.
- A multi-domain app uses SameSite=None with Secure to allow the session cookie to be sent in cross-site requests to a shared API subdomain.
More examples
Secure session cookie options
Configures the session cookie to be httpOnly, secure in production, SameSite=Strict, and expire after one day.
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 1 day
},
}));Signed cookie with cookie-parser
Uses cookie-parser to sign the cookie value so req.signedCookies returns false if the cookie has been modified by the client.
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get('/set', (req, res) => {
res.cookie('userId', '42', { signed: true, httpOnly: true });
res.json({ ok: true });
});
app.get('/get', (req, res) => {
const userId = req.signedCookies.userId; // false if tampered
if (!userId) return res.status(401).json({ error: 'Tampered cookie' });
res.json({ userId });
});Cross-site cookie for embedded API
Sets SameSite=None with Secure=true to allow the session cookie to be sent in cross-site requests, required for iFramed apps or third-party API widgets.
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // required for SameSite=None
sameSite: 'none', // allow cross-site — only if really needed
maxAge: 3600000,
},
}));
Discussion