CVE-2026-64728
Overview
Background
- SameSite cookies
- A cookie attribute (Strict/Lax/None) that withholds cookies from cross-site requests to defend against CSRF and cross-site tracking.
- site-for-cookies
- The first-party URL WebKit computes per document to decide whether an outgoing request is same-site; getting it wrong misclassifies cross-site as same-site.
- srcdoc / about:blank inheritance
- These frames inherit their security origin from their owner; their real origin is the parent’s, not the top document’s.
Root Cause Analysis
This fixes an incorrect site-for-cookies computation that let SameSite-restricted cookies leak to cross-site requests from a srcdoc iframe, violating iframe/cookie policy. FrameLoader::setFirstPartyForCookies walks descendant frames and assigns each document’s siteForCookies. For a frame whose document inherits its security origin from its owner (about:srcdoc / about:blank — SecurityPolicy::shouldInheritSecurityOriginFromOwner), the pre-patch code lumped that case together with the registrable-domain match and, when it matched, set the srcdoc document’s siteForCookies to the top-level url. For a srcdoc iframe nested inside a CROSS-ORIGIN iframe, that is wrong: the srcdoc inherits its parent’s (cross-origin) origin, so requests it makes back to the top-level origin are cross-site and must not carry SameSite=Strict/Lax cookies. Because its site-for-cookies was set as if same-site with the top origin, those requests were treated as same-site and received the restricted cookies.
The fix instead, for the inherit-from-owner case, sets the frame document’s siteForCookies to its PARENT frame’s siteForCookies (parent->document()->siteForCookies()), so a srcdoc inside a cross-origin iframe is correctly cross-site to the top origin.
The restored invariant is that a srcdoc/about:blank frame’s first-party-for-cookies follows its actual parent, not the top document. The layout tests confirm SameSite Strict/Lax cookies are withheld from fetch and <img> requests from such a nested srcdoc while the SameSite=None cookie still flows. Established by the diff.
Attack Path
- Embed a cross-origin iframe of the victim's site An attacker page (or a page the attacker controls a subframe of) loads a cross-origin iframe relative to the victim top-level origin.
- Create a nested srcdoc iframe That cross-origin iframe creates a srcdoc iframe, which inherits the cross-origin parent’s origin.
- Issue a request back to the victim origin The srcdoc performs a fetch()/<img> request to the victim’s top-level origin with credentials.
- Leak SameSite cookies cross-site Pre-patch the srcdoc’s site-for-cookies was the top origin, so the cross-site request was treated as same-site and carried SameSite=Strict/Lax cookies for the victim, leaking them (now withheld).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
FrameLoader::setFirstPartyForCookiesSource/WebCore/loader/FrameLoader.cpp |
modified | For frames that inherit their origin from the owner (srcdoc/about:blank), sets the document's siteForCookies to the parent frame's siteForCookies instead of the top-level url, so a srcdoc nested in a cross-origin iframe is correctly treated as cross-site for SameSite cookie purposes. |
Files Changed
LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txtLayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.htmlLayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txtLayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.htmlLayoutTests/http/tests/cookies/same-site/resources/record-image-cookies.pyLayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.htmlLayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.htmlSource/WebCore/loader/FrameLoader.cpp
Audit Directions
- Origin-inheritance cases in the loaderAudit every shouldInheritSecurityOriginFromOwner branch for security decisions (cookies, referrer, CSP) that use the top document instead of the actual inheriting parent.
- Nested-frame first-party computationTest site-for-cookies for srcdoc/about:blank frames nested at varying depths inside cross-origin iframes, not just direct children of the top frame.
Patch
diff --git a/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
new file mode 100644
index 000000000000..2cb25ce2f4a6
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
@@ -0,0 +1,13 @@
+Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with a fetch() initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so a request from the srcdoc back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Cookies sent with cross-site fetch initiated from srcdoc iframe inside cross-origin iframe:
+PASS Do not have cookie "strict".
+PASS Do not have cookie "lax".
+PASS Has cookie "implicit-default" with value 9.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html
new file mode 100644
index 000000000000..936fde8bb3e1
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+<script src="../resources/cookie-utilities.js"></script>
+</head>
+<body>
+<script>
+window.jsTestIsAsync = true;
+
+description("Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with a fetch() initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so a request from the srcdoc back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.");
+
+const kCookieValue = "9";
+
+function checkCookies(cookies)
+{
+ debug("Cookies sent with cross-site fetch initiated from srcdoc iframe inside cross-origin iframe:");
+ if (cookies.error) {
+ testFailed(`fetch from srcdoc failed: ${cookies.error}`);
+ return;
+ }
+ if (cookies.strict === undefined)
+ testPassed('Do not have cookie "strict".');
+ else
+ testFailed(`Should not have cookie "strict". But do with value ${cookies.strict}.`);
+
+ if (cookies.lax === undefined)
+ testPassed('Do not have cookie "lax".');
+ else
+ testFailed(`Should not have cookie "lax". But do with value ${cookies.lax}.`);
+
+ if (cookies["implicit-default"] === kCookieValue)
+ testPassed(`Has cookie "implicit-default" with value ${kCookieValue}.`);
+ else
+ testFailed(`Should have cookie "implicit-default" with value ${kCookieValue}. Got ${cookies["implicit-default"]}.`);
+}
+
+async function runTest()
+{
+ await resetCookies();
+ await setCookie("strict", kCookieValue, {"SameSite": "Strict", "Max-Age": 100, "path": "/"});
+ await setCookie("lax", kCookieValue, {"SameSite": "Lax", "Max-Age": 100, "path": "/"});
+ await setCookie("implicit-default", kCookieValue, {"SameSite": null, "Max-Age": 100, "path": "/"});
+
+ let cookiesPromise = new Promise((resolve) => {
+ window.addEventListener("message", (event) => {
+ if (event.data && event.data.type === "cookies-from-srcdoc")
+ resolve(event.data.cookies);
+ }, {once: true});
+ });
+
+ let attackerIframe = document.createElement("iframe");
+ attackerIframe.src = "http://localhost:8000/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html";
+ document.body.appendChild(attackerIframe);
+
+ let cookies = await cookiesPromise;
+ checkCookies(cookies);
+
+ document.body.removeChild(attackerIframe);
+
+ await resetCookies();
+ finishJSTest();
+}
+runTest();
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
new file mode 100644
index 000000000000..b1683955995f
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
@@ -0,0 +1,13 @@
+Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with an <img> sub-resource request initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so the image request back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Cookies sent with cross-site <img> request initiated from srcdoc iframe inside cross-origin iframe:
+PASS Do not have cookie "strict".
+PASS Do not have cookie "lax".
+PASS Has cookie "implicit-default" with value 9.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html
new file mode 100644
index 000000000000..16e76c2c5593
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+<script src="../resources/cookie-utilities.js"></script>
+</head>
+<body>
+<script>
+window.jsTestIsAsync = true;
+
+description("Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with an <img> sub-resource request initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so the image request back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.");
+
+const kCookieValue = "9";
+const kRecorderBase = "/cookies/same-site/resources/record-image-cookies.py";
+const kToken = "srcdoc-img-test-" + Date.now();
+
+async function fetchRecordedCookies()
+{
+ let response = await fetch(`${kRecorderBase}?mode=read&token=${encodeURIComponent(kToken)}`, {credentials: "same-origin"});
+ return response.json();
+}
+
+async function resetRecordedCookies()
+{
+ await fetch(`${kRecorderBase}?mode=reset&token=${encodeURIComponent(kToken)}`, {credentials: "same-origin"});
+}
+
+function checkCookies(cookies)
+{
+ debug("Cookies sent with cross-site <img> request initiated from srcdoc iframe inside cross-origin iframe:");
+ if (cookies.strict === undefined)
+ testPassed('Do not have cookie "strict".');
+ else
+ testFailed(`Should not have cookie "strict". But do with value ${cookies.strict}.`);
+
+ if (cookies.lax === undefined)
+ testPassed('Do not have cookie "lax".');
+ else
+ testFailed(`Should not have cookie "lax". But do with value ${cookies.lax}.`);
+
+ // Positive control: WebKit treats a cookie with no SameSite attribute as
+ // SameSite=None (see coreSameSitePolicy() in CookieCocoa.mm), so it must
+ // still be sent on a cross-site image request that includes credentials.
+ // Without this assertion, the negative checks above could pass simply
+ // because cookies aren't being delivered at all.
+ if (cookies["implicit-default"] === kCookieValue)
+ testPassed(`Has cookie "implicit-default" with value ${kCookieValue}.`);
+ else
+ testFailed(`Should have cookie "implicit-default" with value ${kCookieValue}. Got ${cookies["implicit-default"]}.`);
+}
+
+async function runTest()
+{
+ await resetCookies();
+ await resetRecordedCookies();
+ await setCookie("strict", kCookieValue, {"SameSite": "Strict", "Max-Age": 100, "path": "/"});
+ await setCookie("lax", kCookieValue, {"SameSite": "Lax", "Max-Age": 100, "path": "/"});
+ await setCookie("implicit-default", kCookieValue, {"SameSite": null, "Max-Age": 100, "path": "/"});
+
+ let imgLoadedPromise = new Promise((resolve, reject) => {
+ window.addEventListener("message", (event) => {
+ if (!event.data)
+ return;
+ if (event.data.type === "srcdoc-img-loaded")
+ resolve();
+ else if (event.data.type === "srcdoc-img-error")
+ reject(new Error("srcdoc <img> failed to load"));
+ }, {once: false});
+ });
+
+ let attackerIframe = document.createElement("iframe");
+ attackerIframe.src = "http://localhost:8000/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html?token=" + encodeURIComponent(kToken);
+ document.body.appendChild(attackerIframe);
+
+ try {
+ await imgLoadedPromise;
+ } catch (e) {
+ testFailed(String(e));
+ document.body.removeChild(attackerIframe);
+ await resetCookies();
+ finishJSTest();
+ return;
+ }