CVE-2026-64728
Overview
Impact:
Description: Maliciously crafted web content may violate iframe sandboxing policy
Researchers: an anonymous researcher
Key insight. The bug was in origin reasoning, not memory: the inherit-from-owner case (srcdoc/about:blank) was lumped with the registrable-domain match and given the top-level url as its site-for-cookies. The fix instead inherits the parent frame’s site-for-cookies, so a srcdoc nested inside a cross-origin iframe is correctly cross-site to the top origin.
Patch Details
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;
+ }
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64728 |
| Bugzilla | 313220 |
| Component | WebCore |
| Bug Class | CrossOrigin |
| Severity | medium |
| Commit | c52bbb5187e1602b… |
| Advisory | Apple Advisory |
Terminology
- 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
1. 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.
2. Create a nested srcdoc iframe
That cross-origin iframe creates a srcdoc iframe, which inherits the cross-origin parent’s origin.
3. Issue a request back to the victim origin
The srcdoc performs a fetch()/<img> request to the victim’s top-level origin with credentials.
4. 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
A cross-site cookie leak: SameSite=Strict/Lax cookies for a victim origin are sent on requests from a nested srcdoc iframe that is actually cross-site, enabling CSRF-style actions and cross-site information leakage that SameSite is meant to prevent. Exploitable purely from an attacker page embedding the victim — no memory corruption, high reliability.
Audit Directions
Origin-inheritance cases in the loader
Audit every shouldInheritSecurityOriginFromOwner branch for security decisions (cookies, referrer, CSP) that use the top document instead of the actual inheriting parent.
Nested-frame first-party computation
Test site-for-cookies for srcdoc/about:blank frames nested at varying depths inside cross-origin iframes, not just direct children of the top frame.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
FrameLoader::setFirstPartyForCookies |
Source/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
Source Files
Source/WebCore/loader/FrameLoader.cpp
Test Files
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.html
Regression Test
The fix ships this proof-of-concept / regression test:
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;
+ }
+
+ let cookies = await fetchRecordedCookies();
+ checkCookies(cookies);
+
+ document.body.removeChild(attackerIframe);
+
+ await resetRecordedCookies();
+ await resetCookies();
+ finishJSTest();
+}
+runTest();
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/cookies/same-site/resources/record-image-cookies.py b/LayoutTests/http/tests/cookies/same-site/resources/record-image-cookies.py
new file mode 100755
index 000000000000..0795ddd34a90
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/resources/record-image-cookies.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+#
+# Helper for img-from-srcdoc-iframe-inside-cross-origin-iframe.html
+# (rdar://175498842 / https://bugs.webkit.org/show_bug.cgi?id=313220).
+#
+# Modes:
+# ?mode=reset&token=<id> ............ clear any previously recorded cookies for <id>
+# ?mode=record&token=<id> ........... record incoming Cookie header to a temp file keyed by <id>; respond with a 1x1 PNG
+# ?mode=read&token=<id> ............. respond with JSON of cookies recorded for <id>
+
+import json
+import os
+import sys
+import tempfile
+from urllib.parse import parse_qs
+
+file = __file__.split(':/cygwin')[-1]
+http_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(os.path.dirname(file)))))
+sys.path.insert(0, http_root)
+
+from resources.portabilityLayer import get_cookies, get_state, set_state
+
+query = parse_qs(os.environ.get('QUERY_STRING', ''), keep_blank_values=True)
+mode = query.get('mode', ['record'])[0]
+token = query.get('token', ['default'])[0]
+tmp_file = os.path.join(tempfile.gettempdir(), 'srcdoc-img-cookies-' + token + '.json')
+
+origin = os.environ.get('HTTP_ORIGIN', '*')
+
+if mode == 'reset':
+ set_state(tmp_file, '{}')
+ sys.stdout.write(
+ 'Content-Type: text/plain\r\n'
+ 'Cache-Control: no-store\r\n'
+ 'Access-Control-Allow-Origin: {}\r\n'
+ 'Access-Control-Allow-Credentials: true\r\n\r\n'
+ 'reset'.format(origin)
+ )
+ sys.exit(0)
+
+if mode == 'read':
+ cookies_json = get_state(tmp_file, '{}')
+ sys.stdout.write(
+ 'Content-Type: application/json\r\n'
+ 'Cache-Control: no-store\r\n'
+ 'Access-Control-Allow-Origin: {}\r\n'
+ 'Access-Control-Allow-Credentials: true\r\n\r\n'
+ '{}'.format(origin, cookies_json)
+ )
+ sys.exit(0)
+
+# mode == 'record' (default): record incoming cookies and serve a 1x1 transparent PNG.
+cookies = get_cookies()
+set_state(tmp_file, json.dumps(cookies))
+
+png = bytes.fromhex(
+ '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489'
+ '0000000d4944415478da63f8ffff3f0005fe02fe2c8c5d420000000049454e44ae'
+ '426082'
+)
+sys.stdout.buffer.write(
+ ('Content-Type: image/png\r\n'
+ 'Cache-Control: no-store\r\n'
+ 'Content-Length: {}\r\n\r\n'.format(len(png))).encode()
+)
+sys.stdout.buffer.write(png)
+sys.exit(0)
diff --git a/LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html b/LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html
new file mode 100644
index 000000000000..219c37fbd6df
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Attacker iframe (cross-origin to the top-level victim) that creates a srcdoc loading an img</title>
+</head>
+<body>
+<script>
... (truncated)