Cookie Security: HttpOnly, Secure, SameSite, __Host-
Cookie Security: HttpOnly, Secure, SameSite, __Host-
Cookies are the core state mechanism of HTTP. The flags HttpOnly, Secure, SameSite and the __Host-/__Secure- prefixes decide whether XSS can steal a session, whether CSRF works, and whether a cookie can ride on HTTP. This post is a full walkthrough of the RFC 6265bis attributes with PHP, Express, and nginx examples.
HttpOnly
HttpOnly prevents JavaScript from reading the cookie through document.cookie. Mandatory on session cookies — without it any XSS steals the token. Reference: RFC 6265 §4.1.2.6.
Set-Cookie: session=abc123; HttpOnly
Secure
Secure restricts the cookie to SSL/TLS проверку. Without it, the browser will send the cookie over HTTP — a gift to any on-path attacker. Mandatory on every HTTPS cookie.
Set-Cookie: session=abc123; HttpOnly; Secure
SameSite
SameSite mitigates CSRF. Three values:
Strict— never sent on cross-site requests, not even when clicking a linkLax— sent on top-level GET navigations, not on cross-site POST or iframes. Default in modern browsers.None— always sent; requires Secure
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
Lax fits standard web-app sessions, Strict is for critical operations (banking), None is only for cookies used in third-party iframes (rare).
__Host- and __Secure- prefixes
Prefixes are browser-enforced markers:
__Secure-— cookie must carry Secure. Stops an attacker on an adjacent subdomain from overwriting your named cookie without Secure.__Host-— Secure + Path=/ + no Domain. Maximum isolation: subdomains can't shadow cookies at the parent origin.
Set-Cookie: __Host-session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
Max-Age vs Expires
Max-Age is a delta in seconds, UTC-safe. Expires is an absolute GMT date. Prefer Max-Age:
Set-Cookie: session=abc; Max-Age=3600 ← 1 hour
Set-Cookie: session=abc; Max-Age=0 ← delete
PHP
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
session_start();
setcookie('csrf_token', $token, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
Express / NestJS
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600_000,
},
}));
res.cookie('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600_000,
path: '/',
});
Session fixation and regenerate_id
Always call session_regenerate_id(true) (PHP) or equivalent after login — otherwise an attacker can pre-plant a session ID.
if (loginSuccess($user, $pass)) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
}
Analysing cookies
curl -I https://example.com | grep -i set-cookie
The enterno.io Cookie Security Analyzer inspects every cookie, flags missing attributes and trackers, and assigns a grade.
FAQ
Is SameSite=Lax safe? For most apps, yes. For banking-grade operations use Strict plus a CSRF token.
Why doesn't my cookie show up in cross-domain fetch? Requires credentials: 'include', CORS with Allow-Credentials: true, and SameSite=None; Secure.
Does __Host- break subdomains? Yes — scoped to the exact host. app.example.com and API документацию.example.com need separate cookies.
JWT in a cookie? Yes — safer than localStorage (HttpOnly defeats XSS). Pair with CSRF token or SameSite=Strict.
Conclusion
Canonical session cookie: HttpOnly; Secure; SameSite=Strict; Path=/; __Host- prefix. Audit with the Cookie Analyzer and the Security Scanner. Related: Clickjacking, XSS protection.
Check your website right now
Check now →