Lukas' Notes

web security

Definition

SameSite Cookie

The SameSite attribute of an HTTP cookie determines whether the cookie is attached to cross-site requests — requests where the site that initiates the request differs from the site the cookie belongs to. It has four possible values:

ValueBehaviour
Noneattach to cross-site requests; Secure must be enabled
Laxattach only to top-level cross-site navigations using a safe method (GET)
Strictnever attach to cross-site requests
(unspecified)usually defaults to Lax after 2 minutes

A request from example.com that embeds an image from pics.com crosses sites; whether the cookie on pics.com is sent to pics.com along with that image fetch is exactly what SameSite decides.

Site Test

Obs

The decision is a two-place comparison, not “the from site alone”. A cookie lives in the jar of some target site to; a request originates from a page on site from. The browser asks: is from the same site as to?

  • from == to (same site) ⇒ the request is same-site ⇒ the cookie is attached regardless of SameSite value (the attribute only governs cross-site requests).
  • from != to (different site) ⇒ the request is cross-site ⇒ the value decides: None attaches, Strict omits, Lax attaches only for a top-level safe-method navigation, unspecified ≈ Lax after 2 minutes.

For one fixed cookie the target site to never changes, so across the lecture examples only the initiator (b.com, foo.bar.a.com, window.open, …) varies — which is why it looks like only the from matters. The test is genuinely sameSite(from, to).

Obs

The “same-site” test that SameSite uses is scheme-aware: for cookie purposes, http://a.com and https://a.com are different sites even though they share a registrable domain. This is a stricter notion of same-site than the site boundary used by CORS or CSP.

Top-Level Safe-Method Navigation

The two ingredients in Lax's rule

Lax attaches cookies only on a cross-site request that is both a top-level navigation and uses a safe method. Each qualifier rules out a different way a page can reach a cross-origin resource.

  • Top-level navigation — the request replaces what is shown as the page itself in a browser tab: clicking a link (<a href>), submitting a form with target=_self, or window.open. A request contained inside the page — an <iframe>, an embedded <img>/<script>/<link>, a fetch/XHR call — does not count, because the viewer is still on the initiating page and never visibly navigated to the cross-site one.
  • Safe method — a method that is read-only and side-effect-free by convention; in practice GET (and HEAD). A POST, PUT, DELETE, or other state-changing method does not count even when the navigation itself is top-level, because Lax only relaxes for navigations that look like a user following a link.

So a Lax cookie is attached exactly when a cross-site visit is something a user would see as visiting the target site and uses a read-only verb. An iframe embedding the same URL is neither top-level nor — for POST — safe, hence s3 (iframe) and the Lax+POST hack both drop the cookie.

Decision Diagram

Intuition

The browser walks the tree below for each cookie in the jar of the requested site. The first question is the two-place same-site test; only if it fails — i.e. the request is cross-site — does the SameSite value get consulted.

The same-site branch never consults the value, which is the part that gets forgotten: a same-site POST is sent under Strict, because Strict only governs cross-site requests. The cookie-specific scheme rule can even flip a request that looks same-site (shared registrable domain) into cross-site, as s8 in the example shows — it falls through to the cross-site branch and is then stopped by Strict.

Lax+POST Hack

Lax+POST 2-minute hack

Cookies whose SameSite is left unspecified fall back to plain cross-site top-level POST behaviour for the first 2 minutes after they are set — an exception added for single sign-on flows. During that window the cookie behaves closer to None, so unspecified is not the same as safe. The behaviour is also browser-dependent.

Example

Which cookies get sent, by request type and SameSite

A victim holds four cookies on https://a.com (s1s4), each set with a different SameSite. From a separate page, the victim triggers an action that requests a.com:

  • s1; Secure; SameSite=Lax — sent ✅
    • the victim clicks a link to https://a.com:8443 from http://b.com, a top-level GET
    • Lax allows top-level cross-site navigations using a safe method, and a link click qualifies
  • s2; HttpOnly; SameSite=Strict — sent ✅
    • the victim submits a POST form to https://a.com from https://foo.bar.a.com
    • Strict blocks only cross-site requests, and foo.bar.a.com shares the registrable domain a.com with a.com — the POST is same-site, so Strict permits it
  • s3; SameSite=Laxnot sent ❌
    • b.com embeds an <iframe src=https://a.com/user>
    • an iframe is not a top-level navigation, and Lax protects embedded subresources — so the cookie stays in the jar
  • s4; SameSite=Lax — sent ✅
    • b.com opens window.open('https://a.com'), a top-level navigation
    • Lax allows top-level cross-site navigations, and window.open qualifies

SameSite=None requires Secure, and the protocol matters for cookies

A second batch illustrates two traps:

  • s5; SameSite=None; HttpOnly (no Secure) — never stored
    • the victim clicks a link to https://a.com from https://b.com
    • SameSite=None mandates Secure; the missing Secure makes the whole Set-Cookie invalid, so the browser drops the cookie before any request is made
  • s6; Domain=a.com; SameSite=Strict — sent ✅
    • the victim navigates directly to https://www.a.com
    • a direct navigation is same-site, so Strict allows it
  • s7; Domain=a.com; SameSite=Strict — sent ✅
    • the victim submits a POST form to https://login.a.com from https://www.a.com
    • despite the POST, the request is same-site (bad.a.com setting and www.a.com / login.a.com all share registrable domain a.com, same scheme), so Strict allows it
  • s8; Domain=a.com; SameSite=Strictnot sent ❌
    • the victim clicks a link to https://a.com from http://bad.a.com
    • the cookie was set over http:// by bad.a.com and is read over https:// to a.com; for cookies the scheme is part of the same-site test, so http://bad.a.com and https://a.com are different sites for cookie purposes despite the shared registrable domain
    • Strict therefore blocks s8, even though bad.a.com and a.com look like the same site under the site definition used by CORS or CSP