Cookie flags

A cookie is a tiny piece of state the server hands to the browser, and the browser hands back on every subsequent request. The cookie itself is just a name and a value. What makes it safe or dangerous is the attributes you set on it.

Three attributes do most of the work: Secure, HttpOnly, and SameSite. Miss any one on a session cookie and you have a specific class of vulnerability.

Defined in RFC 6265 (2011) and the active rfc6265bis draft. A cookie lives in a response header:

Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=7200

The browser stores it. Every later request to a matching URL (same host, matching path, matching scheme if Secure) carries it back in the Cookie header. No crypto, no expiry check on the server by default: whoever holds the cookie value can act as the session it represents.

The three flags

Set-Cookie breakdowna safe session cookie
Set-Cookie: __Host-sid=abc123;Secure;HttpOnly;SameSite=Lax;Path=/;Max-Age=7200;
Secure

only sent over HTTPS. Without this, any HTTP subresource or redirect leaks the cookie.

HttpOnly

JavaScript cannot read it. A stolen XSS cannot exfiltrate the session.

SameSite

sent only on same-site requests and top-level navigations. Default in modern browsers.

Path

sent with every request to the origin. Required by the __Host- prefix.

Max-Age

expires in seconds. Prefer over Expires. Capped at 400 days by RFC 6265bis.

__Host- prefix is not a flag, it is a name rule the browser enforces. A cookie named __Host-* must be Secure, must have Path=/, and must not set a Domain. Breaks the cookie if any condition is missed. Tight.

Everything after the name and value is optional per the spec. None of it is optional in practice:

  • Secure keeps the cookie off plaintext HTTP. Without it, every mixed-content subresource or HTTP redirect leaks the session value. Send on every authenticated cookie.
  • HttpOnly blocks document.cookie and related JavaScript APIs. A stolen XSS still runs, but it cannot exfiltrate the session cookie to an attacker-controlled endpoint.
  • SameSite controls what happens on cross-site requests. See the next section.

SameSite in practice

Three values. Each scenario behaves differently.

SameSite in practice
user on your site navigates to another pagesame-site requestcookie sent
user clicks a link from Google to your sitetop-level cross-site GETcookie sent
image on attacker.com points at your /api/logoutcross-site subresource GETblocked
form on attacker.com POSTs to your /api/transfercross-site POSTblocked

Lax: current browser default. Balances the CSRF protection of Strict with the "I came from Google" use case. Safe for most session cookies.

Chrome made SameSite=Lax the default for unset cookies in February 2020; Firefox and Safari followed. A session cookie without an explicit SameSite behaves as Lax in modern browsers. Set it explicitly anyway: old clients, webviews, and exotic runtimes still might not apply the default.

Third-party widgets (Stripe Checkout, Intercom, embedded SSO) need SameSite=None because their cookies cross sites by design. The browser rejects SameSite=None without Secure; they always ship together.

Name prefixes

rfc6265bis formalises two name prefixes the browser enforces:

  • __Secure-: the cookie must be Secure. If it is not, the browser refuses to store it.
  • __Host-: must be Secure, must have Path=/, and must not have a Domain attribute. Effectively "this cookie belongs to this exact host, no subdomain drift."

__Host- is the strongest cookie-naming pattern the web offers. Use it for session IDs and any cookie that should never leave the exact origin that set it.

Partitioned (CHIPS)

Chrome's third-party cookie deprecation created a gap for legitimate embedded-iframe use cases: a vendor widget on your site needs to remember state between loads. CHIPS (Cookies Having Independent Partitioned State) is the answer. The vendor sets:

Set-Cookie: __Host-widget=abc123; Secure; Path=/; SameSite=None; Partitioned

The browser stores it keyed by the top-level site, not the vendor's own site. Each site that embeds the vendor gets its own separate cookie jar. Useful for session state in embeds without enabling cross-site tracking.

Common mistakes

session cookie without Secure on an HTTPS site

Any mixed-content subresource or user typing http:// sends the cookie in the clear. Send Secure on every authenticated cookie, always.

session cookie without HttpOnly

Any XSS becomes session theft. The cookie is readable from JavaScript. Unless you specifically need to read it from JS (you almost certainly do not), set HttpOnly.

SameSite=None without Secure

Browsers reject the cookie entirely. You end up with no session at all. The error is easy to miss because the Set-Cookie succeeds on the server and the cookie just never shows up on subsequent requests.

Domain attribute on an auth cookie

Domain=example.com on a session cookie leaks it to every subdomain. Any XSS on blog.example.com can read the session for app.example.com. Drop the Domain attribute and use __Host- prefix.

Expires instead of Max-Age

Both work, but Max-Age is a simple integer and avoids the time-zone and clock-skew edge cases of HTTP-date parsing. Prefer it for new code.

lifetime over 400 days

rfc6265bis caps Max-Age at 400 days (34560000 seconds). Values above that are silently clamped by modern browsers. Long-lived sessions need server-side refresh, not infinite cookies.


Check whether every cookie your site sets has the three flags, and whether any use the stronger __Host- naming: scan your domain.