Cookies and session hardening in the browser

SameSite Lax, Strict and None semantics and the Lax-by-default change, __Host- and __Secure- cookie prefixes, partitioned cookies (CHIPS) and storage partitioning, postMessage origin checks, and clickjacking defence with frame-ancestors.

FREELesson·10 min read·Intermediate → Advanced
SameSite cookies__Host- prefixCHIPS partitioned cookiespostMessage origin checkframe-ancestorsclickjacking

01SameSite, precisely

✓What you will learn
  • Predict when a cookie is sent under SameSite Strict, Lax and None, and what changed with Lax-by-default
  • Use `__Host-` and `__Secure-` prefixes to stop cookie tossing
  • Explain partitioned cookies (CHIPS) and storage partitioning for embedded content
  • Write postMessage handlers that check origin and validate data
  • Prevent clickjacking with frame-ancestors

The SameSite attribute controls whether a cookie is attached to requests that a different site initiated. Remember that site means scheme plus registrable domain, so https://app.cyart.io and https://blog.cyart.io are same-site, and since schemeful same-site, http://cyart.io and https://cyart.io are not.

Request from another siteStrictLaxNone (requires Secure)
User clicks a link (top-level GET navigation)Not sentSentSent
Cross-site form POST (top-level)Not sentNot sentSent
<img>, <iframe>, fetch subresourceNot sentNot sentSent
Same-site request (any kind)SentSentSent
  • Strict is the strongest CSRF defence but annoying for a session cookie: a user following a link from email to your app arrives looking logged out. A common pattern is a Lax session cookie plus a Strict cookie required for sensitive actions.
  • Lax blocks cross-site POSTs and subresources, which neutralises classic CSRF, while keeping inbound links working. GET endpoints that change state are still exposed: never change state on GET.
  • None is for cookies that genuinely need cross-site use (embedded widgets, some SSO flows) and must be Secure.

Lax-by-default: since Chrome 80 (2020), Chromium treats cookies with no SameSite attribute as Lax. A temporary "Lax+POST" exception still sends such cookies on top-level cross-site POSTs for 2 minutes after they are set, so SSO form posts kept working. Firefox and Safari did not ship Lax-by-default, so always set SameSite explicitly rather than relying on any browser's default.

!SameSite does not protect against your own subdomains

A cookie that is SameSite=Strict is still sent on requests initiated by forum.cyart.io, because it is the same site. If any subdomain hosts user content or can be taken over, put it on a separate registrable domain (the reason large platforms serve uploads from a different domain).

02Cookie prefixes and partitioned cookies

Cookies have weak integrity. A subdomain can set a cookie for the parent domain, and historically an HTTP page could overwrite a cookie a HTTPS page set. An attacker controlling sub.cyart.io can toss a sid cookie scoped to .cyart.io and fix the victim's session. Cookie name prefixes make the browser enforce rules at set time:

PrefixBrowser requiresProtects against
__Secure-Secure attribute, set from an HTTPS originInsecure origins setting or overwriting the cookie
__Host-Secure, set from HTTPS, no `Domain` attribute, Path=/Insecure origins and sibling or parent subdomains: the cookie is locked to one host
Hardened cookieshttp
Set-Cookie: __Host-sid=k7Qe0m9VbX2pLr4T; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
Set-Cookie: __Host-csrf=Zt5uYw1aQe8rN3cF; Path=/; Secure; SameSite=Strict
Set-Cookie: __Host-widget_prefs=theme%3Ddark; Path=/; Secure; SameSite=None; Partitioned

# Rejected by the browser, never stored:
Set-Cookie: __Host-sid=abc; Path=/; Secure; Domain=cyart.io     <- Domain not allowed with __Host-
Set-Cookie: __Secure-token=abc; Path=/                          <- missing Secure

Partitioned cookies (CHIPS, Cookies Having Independent Partitioned State) handle third-party embeds. A cookie with the Partitioned attribute (plus Secure) set by widget.cyart.io inside an iframe on shop.example is keyed to the pair (top-level site shop.example, cookie host). The same widget embedded on news.example gets a separate, empty jar. The embed keeps state per embedding site, but cannot be used to track a user across sites. Chrome supports it since version 114; Safari and Firefox already block or partition most third-party cookies by default, so CHIPS is the portable way to keep embedded state working.

Storage partitioning applies the same idea to everything else a third-party iframe can store: localStorage, IndexedDB, Cache Storage, service workers and BroadcastChannel are keyed by top-level site, and the HTTP cache is partitioned too. A practical consequence: a widget can no longer share login state across embedding sites through storage. It needs an explicit flow such as a top-level redirect or the Storage Access API.

03postMessage without the foot-guns

window.postMessage is the sanctioned way for cross-origin windows and iframes to talk. It is safe only if both sides name the other origin. The sender passes a targetOrigin so the message is not delivered if the window has navigated somewhere else. The receiver checks event.origin exactly, and treats event.data as untrusted input.

Strict origin check and schema validationjs
const WIDGET_ORIGIN = "https://widget.cyart.io";
const frame = document.getElementById("billing-widget");

window.addEventListener("message", (event) => {
  if (event.origin !== WIDGET_ORIGIN) return;              // exact match: no indexOf, endsWith or regex
  if (event.source !== frame.contentWindow) return;        // and from the frame we embedded
  const msg = event.data;
  if (typeof msg !== "object" || msg === null || msg.type !== "resize") return;
  if (!Number.isInteger(msg.height) || msg.height < 0 || msg.height > 5000) return;
  frame.style.height = msg.height + "px";                  // never innerHTML or eval with message data
});

// Sending: name the receiver, never "*" for anything sensitive
frame.contentWindow.postMessage({ type: "session", csrf: csrfToken }, WIDGET_ORIGIN);
  • Missing origin check means any site that can frame or open your page can drive the handler. Combined with a sink like innerHTML it is DOM XSS; with an action like "save settings" it is CSRF without cookies.
  • targetOrigin: "*"** leaks the payload to whatever document is in that window now, for example after an attacker navigated the iframe away.
  • Weak matching: event.origin.indexOf("cyart.io") !== -1 passes https://cyart.io.attacker.net.

04Clickjacking and frame-ancestors

Clickjacking loads your page in an invisible iframe on an attacker's page and lines up a button the victim thinks they are clicking ("Play video") over your real button ("Delete account", "Approve payment"). The victim's cookies make it an authenticated click. The defence is to tell the browser which sites may frame you.

HeaderExampleNotes
CSP frame-ancestorsContent-Security-Policy: frame-ancestors 'none'Modern control. Supports lists: 'self' https://partner.example. Not honoured in <meta>
X-Frame-OptionsX-Frame-Options: DENY or SAMEORIGINLegacy. ALLOW-FROM is not supported by modern browsers. When both are present, frame-ancestors wins
▭On the whiteboard · Session hardening checklist4 points
01
cookie
__Host-sid | HttpOnly | Secure | SameSite=Lax | short Max-Age | rotated at login
02
actions
Strict cookie or CSRF token for state changes | no state change on GET
03
framing
frame-ancestors 'none' (or explicit partners) | COOP same-origin on auth pages
04
messaging
exact postMessage origins both ways | schema-validate data
◎Interview radar

A classic: "we set SameSite=Lax everywhere, do we still need CSRF tokens?". Good answers: Lax covers cross-site POST and subresources, but not state-changing GETs, not same-site attackers on sibling subdomains, and not every browser's defaults. Then mention __Host- to stop subdomain cookie tossing. That progression from attribute to threat model is what interviewers listen for.

“How to say it in the room

"The session cookie is __Host- prefixed, HttpOnly, Secure and SameSite=Lax, so it is locked to one host and not sent on cross-site POSTs. Sensitive actions also need a Strict cookie or CSRF token, because SameSite treats every subdomain as same-site. Embedded widgets use partitioned cookies, postMessage checks exact origins both ways, and frame-ancestors 'none' stops clickjacking on everything that is not meant to be framed."

Test yourself

A session cookie is set with `SameSite=Lax`. Which request from `https://evil.example` will carry it to `https://app.cyart.io`?

Q1Why can't `__Host-` cookies have a Domain attribute?+

Setting Domain shares the cookie with every subdomain and lets subdomains overwrite it. Requiring no Domain locks the cookie to the exact host that set it, so a compromised sibling cannot toss a replacement.

Q2Is X-Frame-Options still needed?+

Only for very old browsers. Every current browser supports frame-ancestors, which is more flexible. Sending both is harmless, and frame-ancestors takes precedence.

Q3What does CHIPS not protect?+

It isolates third-party cookie state per top-level site. It does not stop the embed from being clickjacked, from receiving forged postMessages, or from leaking data through its own XSS.

Finished this one?
Tick it and it shows as done on the roadmap and the course.