c52bbb5187e1602b26568547b57440f196bba56a Srcdoc iframes bypass SameSite Strict and Lax cookies
Triage note: New test asserts that a fetch() from a srcdoc iframe nested in a cross-origin iframe must not send SameSite=Strict/Lax cookies; previously the srcdoc's inherited origin let cross-site requests receive restricted cookies, defeating CSRF protection.
Contents
The bug at a glance
Any attacker-controlled page that a victim can be lured into loading a cross-origin iframe from can nest a srcdoc iframe and issue credentialed requests (fetch, <img>, form posts) back to a target origin that receive that origin’s SameSite=Strict and SameSite=Lax cookies, defeating the CSRF mitigation SameSite exists to provide. Impact is scoped to authenticated cross-site request forgery — no direct code execution or memory corruption — and requires the victim to already have SameSite-protected cookies for the target, so CVSS 6.5 (network, low complexity, no privileges, user interaction, high integrity impact bounded to same-site request forgery) is appropriate.
SameSite cookies decide same-vs-cross-site by comparing a request’s siteForCookies (a.k.a. firstPartyForCookies) against the target. A srcdoc iframe has no URL of its own — SecurityPolicy::shouldInheritSecurityOriginFromOwner returns true for about:srcdoc — and WebCore used that fact to hand the srcdoc frame the page’s mainFrameURL as its firstPartyForCookies. That is correct only when the srcdoc sits directly under the top frame; when the srcdoc is nested inside a cross-origin iframe, giving it the top-level site’s cookies makes a request that is genuinely cross-site look first-party, so Strict/Lax cookies leak to it. The fix stops inheriting the page URL and instead inherits the srcdoc frame’s actual parent’s siteForCookies, so a srcdoc under a cross-origin ancestor correctly inherits that cross-origin site and its requests back to the top origin are treated as cross-site.
Root cause
FrameLoader::setFirstPartyForCookies walks descendant frames and, for each, decides which siteForCookies to assign. The site-for-cookies value is what the network layer compares against a request’s target to classify the request as same-site (Strict/Lax cookies allowed) or cross-site (only SameSite=None sent). The pre-patch condition assigned the top-level url whenever either shouldInheritSecurityOriginFromOwner(document->url()) was true OR the document URL was same-registrable-domain as the top URL.
srcdoc documents load with the about:srcdoc URL, for which shouldInheritSecurityOriginFromOwner is true by design (a srcdoc inherits its parent’s security origin). So the first clause matched every srcdoc frame and unconditionally set its firstPartyForCookies to the page’s mainFrameURL — the top-level victim origin — regardless of how many, or what kind of, frames sat between the top frame and the srcdoc.
The abuse: attacker gets the victim to load an iframe from attacker.example (cross-origin to the victim top frame victim.example). That cross-origin iframe programmatically creates a nested iframe via the srcdoc attribute. The srcdoc frame inherits attacker.example’s security origin, but because of the buggy clause its siteForCookies was set to victim.example. A credentialed fetch()/<img> from inside the srcdoc back to victim.example was therefore classified same-site, and victim.example’s SameSite=Strict and SameSite=Lax cookies were attached — a cross-site request receiving CSRF-protected cookies.
The fix splits the condition. When shouldInheritSecurityOriginFromOwner is true, instead of using the page url it looks up the srcdoc frame’s parent (dynamicDowncast<LocalFrame>(localFrame->tree().parent())) and copies that parent’s siteForCookies via setSiteForCookies(parent->document()->siteForCookies()). So a srcdoc nested under a cross-origin ancestor inherits that ancestor’s (cross-origin) site-for-cookies, and its requests to the top origin are correctly cross-site. The same-registrable-domain clause (registrableDomain.matches) is preserved unchanged as the else branch, so legitimate same-site subframes are unaffected.
Key code
srcdoc frame inherits its parent’s siteForCookies instead of the page URL
// FrameLoader::setFirstPartyForCookies (per descendant frame)
- if (SecurityPolicy::shouldInheritSecurityOriginFromOwner(protect(localFrame->document())->url()) || registrableDomain.matches(protect(localFrame->document())->url()))
+ if (SecurityPolicy::shouldInheritSecurityOriginFromOwner(protect(localFrame->document())->url())) {
+ if (RefPtr parent = dynamicDowncast<LocalFrame>(localFrame->tree().parent()))
+ protect(localFrame->document())->setSiteForCookies(parent->document()->siteForCookies());
+ } else if (registrableDomain.matches(protect(localFrame->document())->url()))
protect(localFrame->document())->setSiteForCookies(url);
Patch walkthrough
Source/WebCore/loader/FrameLoader.cpp— In setFirstPartyForCookies, the combined OR condition is split. The shouldInheritSecurityOriginFromOwner(document->url()) case no longer assigns the top-level url; it now fetches the frame’s parent LocalFrame and, if present, sets the frame document’s siteForCookies to the parent’s siteForCookies. The same-registrable-domain case (registrableDomain.matches(document->url())) becomes the else branch and still assigns url. This makes a srcdoc inherit the ancestor chain’s cookie site rather than jumping to the page origin, so nesting under a cross-origin frame is honored.LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html— Regression test: the top page at 127.0.0.1:8000 sets strict/lax/implicit-default cookies, then embeds a cross-origin iframe at localhost:8000 which creates a srcdoc that fetch()es back to 127.0.0.1 with credentials. It asserts the strict and lax cookies are NOT returned while the SameSite-absent cookie IS, confirming the request is treated cross-site.LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html— The cross-origin (localhost:8000) attacker frame that builds the nested srcdoc document; the srcdoc issues fetch(…, {credentials:‘include’, mode:‘cors’}) to the top-level 127.0.0.1 origin and postMessages the resulting cookies to window.top, driving the fetch variant of the test.LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html— Parallel test for a non-fetch subresource: a srcdoc-loaded <img> request back to the top origin. It uses a server-side recorder to capture the Cookie header the image request actually carried and applies the same strict/lax-absent, implicit-default-present assertions, closing the subresource path in addition to fetch.LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html— The cross-origin frame for the img variant; it builds a srcdoc that loads an <img> from the recorder endpoint on the top origin and reports load/error to window.top so the harness knows when to read recorded cookies.LayoutTests/http/tests/cookies/same-site/resources/record-image-cookies.py— CGI recorder with reset/record/read modes: on record it captures the incoming Cookie header to a token-keyed temp file and returns a 1x1 PNG; on read it returns the recorded cookies as JSON. This lets the img test observe cookies on a request that has no JS-readable response body.LayoutTests/http/tests/cookies/same-site/*-expected.txt— Expected outputs asserting PASS for ‘Do not have cookie strict’, ‘Do not have cookie lax’, and ‘Has cookie implicit-default with value 9’ — encoding both the negative (restricted cookies withheld) and positive-control (SameSite=None still delivered) invariants.
Background
siteForCookies / firstPartyForCookies — The value a request carries that the network stack compares against the target URL to classify same-site vs cross-site. SameSite=Strict/Lax cookies are attached only to same-site requests; misassigning this value directly controls whether restricted cookies are sent.
shouldInheritSecurityOriginFromOwner — Returns true for URLs like about:srcdoc and about:blank, meaning the document has no URL of its own and inherits its owner’s security origin. The bug was equating ‘inherits origin’ with ‘is first-party to the top-level page’, which fails when the owner chain is cross-origin.
srcdoc iframe — An iframe whose content comes from the srcdoc attribute rather than a fetched URL. It inherits its parent frame’s origin, so it can issue credentialed requests as that parent’s origin — the mismatch that, combined with an inherited page-level cookie site, produced the bypass.
SameSite as CSRF defense — Strict/Lax cookies are the primary browser mitigation for cross-site request forgery. A bypass that makes a cross-site request look same-site re-enables CSRF against sites that relied on SameSite, which is why this is a security fix rather than a spec conformance tweak.
Vulnerability window
- Baseline — setFirstPartyForCookies assigns the top-level page url to any descendant whose document inherits its origin from its owner (or is same-registrable-domain), treating shouldInheritSecurityOriginFromOwner as equivalent to first-party.
- srcdoc mismatch — A srcdoc document loads about:srcdoc, so shouldInheritSecurityOriginFromOwner is true and it receives the page’s mainFrameURL as its cookie site — correct only when the srcdoc’s ancestors are all the top origin.
- Nested cross-origin abuse — A srcdoc placed under a cross-origin iframe still gets the top origin as its cookie site, so credentialed fetch/img requests from it back to the top origin are wrongly classified same-site and receive Strict/Lax cookies.
- Discovery / test — The added layout tests demonstrate strict and lax cookies leaking to a cross-site request initiated inside such a nested srcdoc, with an implicit-default (SameSite=None) positive control.
- Fix — FrameLoader inherits the srcdoc frame’s actual parent’s siteForCookies instead of the page URL, so a cross-origin ancestor’s cookie site propagates down and the request is correctly cross-site; the same-registrable-domain path is untouched.
Proof of concept
Reconstructed from the shipped test (fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html + srcdoc-creator-inside-cross-origin-iframe.html). The victim origin 127.0.0.1:8000 sets strict/lax/implicit-default cookies; a cross-origin iframe at localhost:8000 spawns a srcdoc that fetches echo-json.py on 127.0.0.1 with credentials. On an unpatched build the echoed cookies include strict and lax (bypass); patched, only implicit-default (SameSite=None) comes back. The <img> variant with record-image-cookies.py demonstrates the same for non-fetch subresources.
<!-- Top-level victim page at http://127.0.0.1:8000 with SameSite cookies set -->
<!-- 1) Embed a CROSS-ORIGIN attacker iframe -->
<iframe src="http://localhost:8000/attacker.html"></iframe>
<!-- attacker.html (origin localhost:8000) creates a nested srcdoc: -->
<script>
const fetchURL = "http://127.0.0.1:8000/cookies/resources/echo-json.py";
const srcdoc = `<!DOCTYPE html><body><script>
fetch(${JSON.stringify(fetchURL)}, {credentials: "include", mode: "cors"})
.then(r => r.json())
.then(cookies => window.top.postMessage({type:"cookies-from-srcdoc", cookies}, "*"));
<\/script></body>`;
const f = document.createElement("iframe");
f.srcdoc = srcdoc; // inherits localhost:8000 origin
document.body.appendChild(f);
// Pre-fix: fetch back to 127.0.0.1 is classified same-site,
// so SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are attached.
</script>
Exploitation
- Establish the frame chain — Lure the victim to a page (or ad slot) that embeds a cross-origin iframe under the attacker’s control; that iframe programmatically creates a nested srcdoc iframe, which inherits the cross-origin frame’s origin.
- Issue credentialed cross-site requests — From inside the srcdoc, make fetch(credentials:‘include’), load <img>/subresources, or auto-submit forms targeting a victim origin for which the user holds SameSite=Strict/Lax cookies (session/auth/CSRF tokens).
- Achieve CSRF / read side effects — Because the requests carry the restricted cookies, they act as authenticated same-site actions against the target, enabling state-changing CSRF; where CORS permits, response data can also be read back and postMessaged out. No memory-safety primitive is involved.
Detection & hunting
For defenders and SOC / detection engineers:
- srcdoc under cross-origin ancestor —
- Cookie header on cross-site srcdoc requests —
- siteForCookies vs origin divergence —
Audit directions
- Other origin-inheriting URLs —
- Deep nesting and mixed frame trees —
- Non-cookie first-party consumers —