Cookie flags audit: Secure / HttpOnly / SameSite in 2 lines
Three cookie attributes mitigate three different attack classes. Setting all three on every authentication cookie is the lowest-effort, highest-value security improvement most apps could make. We see them missing on ~30% of the domains we scan.
The three flags
| Flag | What it stops |
|---|---|
Secure | Cookie sent only over HTTPS. Defends against MITM on the network path. |
HttpOnly | Cookie not exposed to JavaScript. Defends against XSS-based theft. |
SameSite=Lax / Strict | Cookie not sent on cross-site requests. Defends against CSRF. |
SameSite values
Strict— never sent on any cross-site request. Best for high-value sites (banking) but breaks the "click a link in an email and stay logged in" flow.Lax— sent on top-level navigations (clicking a link) but not on cross-site POSTs / iframes / ajax. Default for major browsers as of Chrome 80.None— always sent (the old default). MUST be combined withSecure. Required only for legitimate cross-site cookies (third-party SSO, embedded widgets).
The __Host- prefix (the bonus)
A cookie named __Host-Session=... has automatic guarantees enforced by the
browser:
- MUST have
Secure - MUST have
Path=/ - MUST NOT have
Domain=...(so it can't be set or read by subdomains)
This is the cleanest defence against subdomain-takeover-based session theft (cf. our
subdomain takeover article). If a
subdomain is compromised, it can't read or write your __Host- cookie because
the browser refuses to bind it to anything but the exact host.
UnveilScan uses __Host-unveilscan_session and __Host-unveilscan_csrf
for this reason. Cost: zero. Benefit: real.
How to set the flags
nginx
# Doesn't set cookies itself; rewrites the Set-Cookie header from upstream.
# Add flags to every upstream-set cookie:
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=Lax";
Apache (httpd)
Header always edit Set-Cookie ^(.*)$ "$1; Secure; HttpOnly; SameSite=Lax"
Express (Node.js)
// Using cookie-session or express-session
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
secure: true, // HTTPS only
httpOnly: true,
sameSite: 'lax',
maxAge: 86400000,
},
name: '__Host-app_session', // bonus: __Host- prefix
}));
Django
# settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True # default since Django 1.7
SESSION_COOKIE_SAMESITE = 'Lax' # default since Django 2.1
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'Lax'
Spring Boot
# application.properties
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.same-site=lax
PHP
// php.ini
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
// Or per-script
session_set_cookie_params([
'secure' => true, 'httponly' => true, 'samesite' => 'Lax',
]);
Verifying
$ curl -sI https://example.com/login -X POST -d 'user=foo&pass=bar' \
| grep -i set-cookie
Set-Cookie: __Host-app_session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax
All three flags + __Host- prefix + Path=/ = clean.
Common mistakes
- Forgetting Secure on auth cookies when running behind a TLS-terminating reverse proxy. The cookie sees plain HTTP at the upstream server level — frameworks default to no Secure flag. Override explicitly.
- SameSite=None without Secure — modern browsers reject this. Always pair the two.
- SameSite=Strict on session cookies — breaks "click email link → land logged in". Use Lax instead unless you have a hard reason.
- Sending JWT in cookies without HttpOnly — exposes the token to any XSS. Either store JWTs in HttpOnly cookies or accept they live in localStorage with the security trade-off.
How UnveilScan checks
Our headers checker parses every Set-Cookie header in the apex response:
- Missing
Secureon a session-ish cookie name (heuristic:session,auth,token,JSESSIONID,PHPSESSID,laravel_session) → MEDIUM. - Missing
HttpOnlyon session-ish → MEDIUM. - Missing
SameSiteentirely → LOW. - Missing flags on non-session cookies (analytics, prefs) → LOW informational.
Audit your cookies
Free Basic scan parses Set-Cookie headers and reports missing flags by cookie name.
Run a scan