Medium CVSS 6.5 webkit Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionMaliciously crafted web content may violate iframe sandboxing policy
ComponentWebCore
Sub-componentWebCore Loader
Bug ClassCross Origin
Tracker313220
Fix commitc52bbb5187e1 (WebKit/WebKit) +176/-0
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
Creditedan anonymous researcher
Disclosed2026-07-27

Background

`siteForCookies`
the “first party for cookies” URL a document is associated with, used to decide whether an outgoing request is same-site or cross-site for SameSite cookie enforcement.
`srcdoc` iframe
an iframe whose content is supplied inline via the srcdoc attribute; its document URL is about:srcdoc and it inherits its security origin from its parent (owner) frame.
`SecurityPolicy::shouldInheritSecurityOriginFromOwner`
predicate returning true for URLs like about:srcdoc / about:blank that must inherit their origin from the frame that created them rather than deriving it from the URL.
`SameSite` cookies
cookies marked Strict or Lax that browsers withhold from cross-site requests to blunt CSRF and cross-site tracking.

Root Cause Analysis

FrameLoader::setFirstPartyForCookies walks every descendant LocalFrame and assigns each document a siteForCookies. The original code used a single condition that combined SecurityPolicy::shouldInheritSecurityOriginFromOwner(...url()) with registrableDomain.matches(...url()) and, when either was true, called setSiteForCookies(url) with the top-level frame’s first-party url. For an inherited-origin document such as a srcdoc iframe, shouldInheritSecurityOriginFromOwner is true, so the code unconditionally stamped it with the top-level site-for-cookies even when that srcdoc was nested inside a cross-origin iframe — violating the invariant that an inherited-origin document must adopt its actual parent’s first-party context.

The fix splits the branches: when the document inherits its origin from its owner, it now copies siteForCookies from its real parent LocalFrame (parent->document()->siteForCookies()) instead of from the top-level URL, so a srcdoc inside a cross-origin frame correctly carries the cross-site first party. This makes subsequent requests from that srcdoc back to the top-level origin evaluate as cross-site, causing SameSite=Strict/Lax cookies to be withheld as required.

Key insight
The single mistake was treating any origin-inheriting descendant (like a srcdoc iframe) as belonging to the top-level first-party context rather than to its immediate parent’s, letting a cross-origin frame’s srcdoc masquerade as same-site with the top document. The fix inherits siteForCookies from the actual parent frame, restoring the correct cross-site relationship.

Attack Path

  1. Embed cross-origin iframe A top-level victim page at http://127.0.0.1:8000 loads (or is made to load) an attacker-controlled iframe from a cross-origin host such as http://localhost:8000.
  2. Create nested srcdoc The cross-origin attacker frame injects a nested srcdoc iframe, which inherits the attacker frame’s origin but was incorrectly assigned the top-level page’s siteForCookies.
  3. Issue request to victim origin Script in the srcdoc performs a credentialed fetch() or loads an <img> back to the top-level origin http://127.0.0.1:8000.
  4. Same-site misclassification Because the srcdoc carries the top-level first party, the request is judged same-site and SameSite=Strict/Lax cookies for the victim origin are attached.
  5. Exfiltrate cookies The attacker observes the authenticated response (or has a server recorder capture the delivered cookies), defeating the sandbox/cross-site boundary.

Impact Assessment

An attacker who can host cross-origin framed content gains the ability to send SameSite-restricted cookies of the embedding top-level origin on requests originating from an inherited-origin (srcdoc) subframe, enabling CSRF-style authenticated actions and cross-site information disclosure in the browser’s web-content context. The precondition is that the victim top-level page embeds attacker-influenced cross-origin framed content that can create a nested srcdoc. No memory corruption is required; the flaw is purely a same-site policy misclassification that violates iframe sandboxing expectations.

Changed Functions

FunctionChangeNotes
if
LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html
modified

Files Changed

  • LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
  • LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html
  • LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
  • LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html
  • LayoutTests/http/tests/cookies/same-site/resources/record-image-cookies.py
  • LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html
  • LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html
  • Source/WebCore/loader/FrameLoader.cpp

Audit Directions

  • Inherited-origin first-party propagation
    Audit every site-for-cookies / first-party assignment that special-cases about:srcdoc/about:blank (via shouldInheritSecurityOriginFromOwner) to confirm it inherits from the immediate parent rather than the top-level frame.
  • Frame-tree walks that use top-level state
    Review loops over descendant frames (like tree() traversals) that apply a single top-level value uniformly, and verify nested cross-origin ancestors are respected at each level.
  • SameSite classification for nested contexts
    Test SameSite cookie delivery for multi-level nestings (cross-origin iframe containing srcdoc, blob:, and data: documents) to ensure cross-site requests never adopt an ancestor’s same-site first party.
diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp
index 2395f8f250f5..84df478fef01 100644
--- a/Source/WebCore/loader/FrameLoader.cpp
+++ b/Source/WebCore/loader/FrameLoader.cpp
@@ -1298,7 +1298,10 @@ void FrameLoader::setFirstPartyForCookies(const URL& url)
         RefPtr localFrame = dynamicDowncast<LocalFrame>(*descendantFrame);
         if (!localFrame)
             continue;
-        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);
     }
 }
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 &lt;img&gt; 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 &lt;img&gt; 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>
+// Loaded at http://localhost:8000 inside an iframe of a top-level page at
+// http://127.0.0.1:8000. Creates a nested srcdoc iframe; the srcdoc loads an
+// <img> back to the top-level origin and reports load completion to the top
+// frame. The top frame then reads the cookies the image request actually saw
+// from a server-side recorder.
+//
+// rdar://175498842 / https://bugs.webkit.org/show_bug.cgi?id=313220
+
+const params = new URLSearchParams(window.location.search);
+const token = params.get("token") || "default";
+const recorderURL = "http://127.0.0.1:8000/cookies/same-site/resources/record-image-cookies.py?mode=record&token=" + encodeURIComponent(token);
+
+const srcdocContent = `<!DOCTYPE html><body><script>
+let img = new Image();
+img.onload = () => window.top.postMessage({type: "srcdoc-img-loaded"}, "*");
+img.onerror = () => window.top.postMessage({type: "srcdoc-img-error"}, "*");
+img.src = ${JSON.stringify(recorderURL)};
+<\/script></body>`;
+
+let srcdocIframe = document.createElement("iframe");
+srcdocIframe.style.display = "none";
+srcdocIframe.srcdoc = srcdocContent;
+document.body.appendChild(srcdocIframe);
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html b/LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html
new file mode 100644
index 000000000000..8d2002468a56
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Cross-origin iframe that creates a srcdoc</title>
+</head>
+<body>
+<script>
+const fetchURL = "http://127.0.0.1:8000/cookies/resources/echo-json.py";
+const srcdocContent = `<!DOCTYPE html><body><script>
+fetch(${JSON.stringify(fetchURL)}, {credentials: "include", mode: "cors"})
+    .then((response) => response.json())
+    .then((cookies) => {
+        window.top.postMessage({type: "cookies-from-srcdoc", cookies}, "*");
+    })
+    .catch((error) => {
+        window.top.postMessage({type: "cookies-from-srcdoc", cookies: {error: String(error)}}, "*");
+    });
+<\/script></body>`;
+
+let srcdocIframe = document.createElement("iframe");
+srcdocIframe.style.display = "none";
+srcdocIframe.srcdoc = srcdocContent;
+document.body.appendChild(srcdocIframe);
+</script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
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 &lt;img&gt; 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 &lt;img&gt; 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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.