A session cookie without the right flags is a credential stored in an easily-exfiltrated location. The browser cookie jar is accessible to JavaScript by default, sent over unencrypted connections by default, and sent cross-origin by default. Each of these defaults is a risk that a corresponding flag closes. Getting cookie configuration right is not optional for auth cookies — it is the difference between a session that is resistant to XSS theft and CSRF, and one that is trivially exploitable.
HttpOnly: keeping the session out of JavaScript
The HttpOnly flag tells the browser not to expose the cookie to JavaScript. It will not appear in document.cookie, it cannot be read by fetch or XMLHttpRequest directly, and it cannot be exfiltrated by an XSS payload. The cookie is still sent automatically by the browser on HTTP requests to the matching domain and path.
For session cookies, refresh token cookies, and any cookie that your JavaScript does not need to read directly, HttpOnly must be set. The only cookies that legitimately omit HttpOnly are those your frontend JavaScript needs to read — for example, a user preference cookie or a feature flag cookie. Session tokens and auth credentials should never be in that category.
// Express: set session cookie with correct flags
res.cookie('session_id', sessionToken, {
httpOnly: true, // Not accessible to JavaScript
secure: true, // HTTPS only
sameSite: 'lax', // CSRF mitigation
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
path: '/',
});
Secure: HTTPS-only transmission
The Secure flag prevents the cookie from being sent over unencrypted HTTP connections. Without it, a cookie set on https://app.example.com could be sent to http://app.example.com if the user navigates there (via a misconfigured redirect, a stale bookmark, or a downgrade attack). With Secure, the browser only sends the cookie on HTTPS connections.
In 2024, all production auth cookies must have Secure. There is no legitimate reason to send session credentials over plaintext HTTP. For local development, this means running your dev server over HTTPS, or accepting that you will set up a slightly different cookie configuration for development. Do not disable Secure in production to work around a development inconvenience.
SameSite=Lax vs SameSite=Strict
The SameSite attribute controls when cookies are sent on cross-site requests. It is the primary browser-level defense against CSRF attacks.
SameSite=Strict means the cookie is only sent on same-site requests — requests where the top-level page is on the same site as the cookie's domain. A user clicking a link from an external site to your app will not have the cookie sent on that first navigation. This is highly secure but creates a confusing UX: a logged-in user who clicks a link from their email client arrives at your app and appears logged out, then is logged in after a refresh. Not suitable for most web applications.
SameSite=Lax (the browser default as of Chrome 80) allows the cookie to be sent on top-level navigations (clicking links, typing URLs) but blocks it on cross-site subrequests (fetch calls, img tags, form POSTs from other origins). This protects against the classic CSRF attack vector (malicious form on another site POSTing to your API) while keeping user experience intact for normal link navigation.
// The right defaults for session cookies in 2024
function setAuthCookie(res, name, value, options = {}) {
res.cookie(name, value, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
// For APIs on a different subdomain, you may need 'none' with Secure
// sameSite: 'none', // requires secure: true
path: '/',
maxAge: options.maxAge ?? 7 * 24 * 60 * 60 * 1000,
domain: options.domain, // set for cross-subdomain sharing
});
}
The __Host- prefix
The __Host- cookie name prefix enforces stricter constraints at the browser level. A cookie named __Host-session is only accepted if it was set with Secure, has no Domain attribute, and has Path=/. The browser will reject attempts to set a __Host- cookie that violates any of these constraints.
This prevents a subdomain from setting a cookie that shadows the parent domain's auth cookie. Without the prefix, evil.example.com could set a cookie named session for .example.com that overrides your auth session cookie. With __Host-session, that attack is impossible — the prefix prevents the Domain attribute from being set.
// Setting a __Host- prefixed cookie
res.cookie('__Host-session', sessionToken, {
httpOnly: true,
secure: true, // Required for __Host- prefix
sameSite: 'strict',
path: '/', // Required for __Host- prefix (must be /)
// domain: must NOT be set — __Host- prefix forbids it
});
Similarly, __Secure- prefix requires only that the cookie be sent over HTTPS and set with Secure. It does not restrict the Domain or Path attributes. Use __Host- for the strongest protection, __Secure- when you need cross-subdomain sharing.
Cookie partitioning (CHIPS)
Cookies Have Independent Partitioned State (CHIPS), available in Chrome 114+, is a new mechanism for third-party cookies. A cookie with the Partitioned attribute is stored and accessed in a partition keyed by the top-level site. The same third-party iframe on different top-level sites gets different cookie jars. This is primarily relevant for embedded widgets and third-party auth flows, not for first-party session cookies.
If you are building an embeddable widget (a login button, a payment form) that needs to maintain state across page loads, Partitioned allows cookie-based state without being affected by third-party cookie blocking. First-party session cookies are unaffected by partitioning — they operate in the first-party partition by definition.
The SameSite and CSRF interaction
SameSite=Lax protects against cross-site form POST attacks, which covers the original CSRF threat model. But it does not protect against attacks that exploit the 2-minute window during which Chrome 80+ treats same-site cookies as Lax but allows them on top-level cross-site navigations. For state-mutating requests that are vulnerable to navigation-triggered CSRF, an additional CSRF token check is still warranted even with SameSite=Lax.
The most robust approach is SameSite=Lax for the session cookie plus a synchronized token pattern for state-mutating requests. The server generates a CSRF token, stores it in the session, and embeds it in every form. The client includes it in the X-CSRF-Token header or form body. Cross-origin requests cannot read the token because of the Same-Origin Policy, so an attacker cannot forge a valid request.