The Secure flag
A cookie with the Secure flag is only sent over HTTPS connections. Without it, the browser will transmit the cookie over plain HTTP, allowing any network observer to intercept it.
Set-Cookie: session=abc123; Secure
This flag should be set on every cookie in production. There is no legitimate reason to send cookies over unencrypted HTTP. Even if your site redirects HTTP to HTTPS, an attacker who intercepts the initial HTTP request will see any cookies that lack the Secure flag.
Note that Secure does not encrypt the cookie value -- it only restricts the transport. The cookie is still visible in browser developer tools and to any JavaScript on the page (unless HttpOnly is also set).
The HttpOnly flag
A cookie with HttpOnly cannot be accessed by JavaScript via document.cookie, the Cookie Store API, or any other client-side API. It is only sent with HTTP requests.
Set-Cookie: session=abc123; Secure; HttpOnly
This is the primary defense against session theft via XSS. If an attacker injects a script into your page, they cannot read HttpOnly cookies and exfiltrate them to their server. The cookie is still sent with every request to your origin, but the attacker's JavaScript cannot touch it.
Set HttpOnly on all session cookies and authentication tokens. Only omit it for cookies that JavaScript genuinely needs to read, such as a CSRF token (which needs to be read by JavaScript to include in request headers) or a UI preference cookie.
SameSite attribute
The SameSite attribute controls whether a cookie is sent with cross-site requests, providing protection against CSRF (cross-site request forgery) attacks.
SameSite=Strict
The cookie is never sent with cross-site requests. If a user clicks a link to your site from an external page, the cookie is not included in that initial request. This is the most secure setting but can hurt usability -- users following links from emails or other sites will arrive unauthenticated on the first request.
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict
SameSite=Lax
The cookie is sent with top-level navigations (clicking a link) but not with cross-site subrequests (images, iframes, AJAX/fetch). This provides CSRF protection for state-changing requests (POST, PUT, DELETE) while preserving usability for navigation.
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax
Lax is the default in modern browsers (Chrome 80+, Firefox 69+, Edge 80+). If you omit SameSite entirely, the browser treats it as Lax. However, explicitly setting it is best practice because older browsers default to None (no restriction).
SameSite=None
The cookie is sent with all cross-site requests, including in iframes and AJAX. This is required for legitimate cross-site use cases like third-party embedded widgets, single sign-on across domains, and payment provider integrations.
Set-Cookie: widget_session=xyz; Secure; HttpOnly; SameSite=None
SameSite=None requires the Secure flag. Browsers reject SameSite=None cookies without Secure. Only use this when cross-site sending is genuinely needed.
Cookie prefixes: __Host- and __Secure-
Cookie prefixes are a defense-in-depth mechanism that prevents certain types of cookie injection and scope manipulation attacks.
__Host- prefix
A cookie whose name starts with __Host- must be:
- Set with the
Secureflag - Sent from a secure (HTTPS) origin
- Not include a
Domainattribute (bound to the exact host, not subdomains) - Have
Path=/
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/
This is the strongest cookie binding available. It prevents a subdomain from overwriting the cookie (because the cookie has no Domain attribute, it is bound to the exact host). It also prevents an attacker who controls a sibling subdomain from setting a cookie that shadows yours.
__Secure- prefix
A cookie whose name starts with __Secure- must be set with the Secure flag from a secure origin. It is less restrictive than __Host- because it allows a Domain attribute.
Set-Cookie: __Secure-prefs=dark; Secure; SameSite=Lax; Domain=example.com; Path=/
Use __Secure- when you need a cookie to be shared across subdomains but still want the guarantee that it was originally set over HTTPS.
Session cookie hardening checklist
For a secure session cookie, apply all of the following:
Set-Cookie: __Host-session=<token>; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400
__Host-prefix: Binds to exact host, prevents subdomain cookie injection.Secure: HTTPS transport only.HttpOnly: No JavaScript access, prevents XSS-based session theft.SameSite=Lax: Prevents CSRF on state-changing requests while allowing link navigation.Path=/: Required by __Host- prefix; also ensures the cookie is available site-wide.Max-Age: Explicit expiry. PreferMax-AgeoverExpires-- it is not affected by clock skew between server and client. For session cookies, use the shortest lifetime your application can tolerate.
Additional measures
- Rotate session tokens after login and privilege escalation. Invalidate the old token server-side.
- Use a strong random generator for session token values. At least 128 bits of entropy from a cryptographically secure PRNG.
- Set short lifetimes and use refresh mechanisms rather than long-lived session cookies.
- Implement server-side session invalidation. Deleting a cookie client-side does not protect against stolen tokens -- the server must reject the old value.
Framework examples
Express.js (Node):
app.use(session({
name: '__Host-session',
cookie: {
secure: true,
httpOnly: true,
sameSite: 'lax',
path: '/',
maxAge: 86400000 // 24 hours in milliseconds
},
// ... store configuration
}));
Nginx proxy (setting cookie flags on upstream response):
# Add flags to cookies from a backend that does not set them
proxy_cookie_flags ~ secure httponly samesite=lax;
Apache (mod_headers):
# Append Secure and HttpOnly to all Set-Cookie headers
Header always edit Set-Cookie ^(.*)$ "$1; Secure; HttpOnly; SameSite=Lax"