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.
01SameSite, precisely
- 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 site | Strict | Lax | None (requires Secure) |
|---|---|---|---|
| User clicks a link (top-level GET navigation) | Not sent | Sent | Sent |
| Cross-site form POST (top-level) | Not sent | Not sent | Sent |
<img>, <iframe>, fetch subresource | Not sent | Not sent | Sent |
| Same-site request (any kind) | Sent | Sent | Sent |
- 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.
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:
| Prefix | Browser requires | Protects against |
|---|---|---|
__Secure- | Secure attribute, set from an HTTPS origin | Insecure 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 |
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 SecurePartitioned 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.
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
innerHTMLit 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") !== -1passeshttps://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.
| Header | Example | Notes |
|---|---|---|
CSP frame-ancestors | Content-Security-Policy: frame-ancestors 'none' | Modern control. Supports lists: 'self' https://partner.example. Not honoured in <meta> |
X-Frame-Options | X-Frame-Options: DENY or SAMEORIGIN | Legacy. ALLOW-FROM is not supported by modern browsers. When both are present, frame-ancestors wins |
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.
"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."
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.