High CVSS 5.5 webkit Cross Origin 🔧 Commit mapped

Overview

High
Severity
5.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionAn app may be able to access sensitive user data
ComponentWebCore
Sub-componentWebCore Loader
Bug ClassCross Origin
Tracker311228
Fix commit093f34607a3e (WebKit/WebKit) +61/-1
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedCantina
Disclosed2026-05-11

Background

SameSite cookies
A cookie attribute (Strict/Lax/None) controlling whether a cookie is sent on cross-site requests; a core CSRF/tracking defense.
Same-site vs same-origin
Same-site compares registrable domains (site); same-origin compares scheme+host+port. SameSite eligibility is a site-level check that depends on the request initiator.
Request initiator / requester
The document that initiated a load; its site is what a request should be classified same-site against.
shouldInheritSecurityOriginFromOwner
True for about:srcdoc/about:blank documents that inherit their owner’s origin; such initiators are handled conservatively (nullptr) here.

Root Cause Analysis

FrameLoader::load computed and attached SameSite cookie information to the outgoing request by calling addSameSiteInfoToRequestIfNeeded(loader->request()) WITHOUT passing the request’s initiator. As the commit title states (‘Initiator-omitted samesite classification can lead to SameSite=Strict cookie cross-site leakage’), omitting the initiator means the same-site determination is not made against the document that actually initiated the load, so a cross-site request could be misclassified as same-site and therefore have the target origin’s SameSite=Strict/Lax cookies attached — leaking those cookies to a cross-site context.

The fix takes Ref initiator = request.requester() and passes it to addSameSiteInfoToRequestIfNeeded, but passes nullptr when the initiator inherits its security origin from its owner (about:srcdoc/about:blank, via SecurityPolicy::shouldInheritSecurityOriginFromOwner) so such frames are treated conservatively instead of as a concrete same-site initiator.

The restored invariant is that SameSite eligibility is computed against the true initiating document’s site. INFERENCE: the addSameSiteInfoToRequestIfNeeded overload that consumes the initiator and the exact same-site comparison are outside this hunk; the diff establishes that the initiator was omitted and is now supplied.

Key insight
SameSite classification must be computed against the request’s real initiator; omitting the initiator collapses to a classification that can treat cross-site loads as same-site and leak SameSite=Strict cookies.

Attack Path

  1. Attacker initiates a cross-site load An attacker-controlled document triggers a navigation/subresource load targeting a victim origin for which the user holds SameSite=Strict/Lax cookies.
  2. Same-site classification omits the initiator Pre-patch, addSameSiteInfoToRequestIfNeeded is called without the initiator, so the request’s same-site status is computed on a default/omitted basis.
  3. Cross-site request misclassified as same-site The classification treats the request as same-site, making SameSite-restricted cookies eligible.
  4. Leak SameSite cookies cross-site The victim origin’s SameSite=Strict/Lax cookies are attached to the cross-site request and delivered to the attacker-influenced endpoint, defeating the CSRF/cookie isolation the SameSite attribute provides.

Impact Assessment

This is an information-disclosure / CSRF-enabling issue, not a memory-safety bug: SameSite=Strict cookies (which exist specifically to block cross-site cookie attachment) can be leaked onto cross-site requests, enabling session riding / CSRF or exfiltration of authenticated state. It is high severity because it undermines a primary CSRF mitigation, but it requires the victim to have relevant cookies and the attacker to initiate the cross-site load; there is no memory corruption or sandbox escape.

Changed Functions

FunctionChangeNotes
while
Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
modified

Files Changed

  • Source/WebCore/loader/FrameLoader.cpp
  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm

Audit Directions

  • Other addSameSiteInfoToRequestIfNeeded callers
    Grep every call to addSameSiteInfoToRequestIfNeeded and confirm each passes a concrete initiator; any call that omits it (single-argument form) is a candidate for the same misclassification.
  • First-party / same-site computed without the initiator
    Look for isSameSite / firstPartyForCookies / siteForCookies decisions in the loader and cookie layers that default the initiator or use the target URL alone; the tell is a same-site check lacking the requesting document.
  • Origin-inheriting frames in cookie logic
    Audit how about:srcdoc/about:blank (shouldInheritSecurityOriginFromOwner) initiators are treated across the cookie/same-site paths; ensure they are handled conservatively rather than as a concrete same-site party (cross-reference CVE-2026-64728).
diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp
index 027a2c193f9c..e7a2f13a4af4 100644
--- a/Source/WebCore/loader/FrameLoader.cpp
+++ b/Source/WebCore/loader/FrameLoader.cpp
@@ -1816,7 +1816,8 @@ void FrameLoader::load(FrameLoadRequest&& request, std::optional<NavigationReque
 
     if (auto advancedPrivacyProtections = request.advancedPrivacyProtections())
         loader->setOriginatorAdvancedPrivacyProtections(*advancedPrivacyProtections);
-    addSameSiteInfoToRequestIfNeeded(loader->request());
+    Ref initiator = request.requester();
+    addSameSiteInfoToRequestIfNeeded(loader->request(), SecurityPolicy::shouldInheritSecurityOriginFromOwner(initiator->url()) ? nullptr : initiator.ptr());
     applyShouldOpenExternalURLsPolicyToNewDocumentLoader(protect(m_frame), loader, request);
     loader->setIsHandledByAboutSchemeHandler(request.isHandledByAboutSchemeHandler());
 
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
index 2af5a9a27177..84aa1fa89fd9 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
@@ -1227,3 +1227,62 @@ TEST(WKHTTPCookieStore, DeletePartitionedCookie)
     Util::run(&done);
 }
 #endif // ENABLE(OPT_IN_PARTITIONED_COOKIES)
+
+TEST(WKHTTPCookieStore, SameSiteStrictCookieNotSentOnCrossSiteNavigation)
+{
+    using namespace TestWebKitAPI;
+    bool receivedSameSiteCheck { false };
+    bool sameSiteCheckHasCookie { false };
+    bool receivedCrossSiteCheck { false };
+    bool crossSiteCheckHasCookie { false };
+    HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> ConnectionTask {
+        while (true) {
+            auto request = co_await connection.awaitableReceiveHTTPRequest();
+            auto path = HTTPServer::parsePath(request);
+            request.append(0);
+
+            if (path.endsWith("/setcookie"_s)) {
+                co_await connection.awaitableSend(
+                    "HTTP/1.1 200 OK\r\n"
+                    "Content-Length: 4\r\n"
+                    "Set-Cookie: id=secret; Path=/; SameSite=Strict\r\n"
+                    "\r\n"
+                    "Done"_s);
+            } else if (path.endsWith("/attacker"_s)) {
+                co_await connection.awaitableSend(HTTPResponse("<html><body>attacker page</body></html>"_s).serialize());
+            } else if (path.endsWith("/same-site-check"_s)) {
+                sameSiteCheckHasCookie = contains(request.span(), "id=secret"_span);
+                co_await connection.awaitableSend(HTTPResponse("checked"_s).serialize());
+                receivedSameSiteCheck = true;
+            } else if (path.endsWith("/cross-site-check"_s)) {
+                crossSiteCheckHasCookie = contains(request.span(), "id=secret"_span);
+                co_await connection.awaitableSend(HTTPResponse("checked"_s).serialize());
+                receivedCrossSiteCheck = true;
+            }
+        }
+    });
+
+    RetainPtr storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+    [storeConfiguration setProxyConfiguration:@{
+        (NSString *)kCFStreamPropertyHTTPProxyHost: @"127.0.0.1",
+        (NSString *)kCFStreamPropertyHTTPProxyPort: @(server.port()),
+    }];
+    RetainPtr dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+    RetainPtr configuration = adoptNS([WKWebViewConfiguration new]);
+    [configuration setWebsiteDataStore:dataStore.get()];
+    RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://victim.example:%d/setcookie", server.port()]]]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://victim.example:%d/same-site-check", server.port()]]]];
+    Util::run(&receivedSameSiteCheck);
+    EXPECT_TRUE(sameSiteCheckHasCookie);
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://attacker.example:%d/attacker", server.port()]]]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://victim.example:%d/cross-site-check", server.port()]]]];
+    Util::run(&receivedCrossSiteCheck);
+    EXPECT_FALSE(crossSiteCheckHasCookie);
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
index 2af5a9a27177..84aa1fa89fd9 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
@@ -1227,3 +1227,62 @@ TEST(WKHTTPCookieStore, DeletePartitionedCookie)
     Util::run(&done);
 }
 #endif // ENABLE(OPT_IN_PARTITIONED_COOKIES)
+
+TEST(WKHTTPCookieStore, SameSiteStrictCookieNotSentOnCrossSiteNavigation)
+{
+    using namespace TestWebKitAPI;
+    bool receivedSameSiteCheck { false };
+    bool sameSiteCheckHasCookie { false };
+    bool receivedCrossSiteCheck { false };
+    bool crossSiteCheckHasCookie { false };
+    HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> ConnectionTask {
+        while (true) {
+            auto request = co_await connection.awaitableReceiveHTTPRequest();
+            auto path = HTTPServer::parsePath(request);
+            request.append(0);
+
+            if (path.endsWith("/setcookie"_s)) {
+                co_await connection.awaitableSend(
+                    "HTTP/1.1 200 OK\r\n"
+                    "Content-Length: 4\r\n"
+                    "Set-Cookie: id=secret; Path=/; SameSite=Strict\r\n"
+                    "\r\n"
+                    "Done"_s);
+            } else if (path.endsWith("/attacker"_s)) {
+                co_await connection.awaitableSend(HTTPResponse("<html><body>attacker page</body></html>"_s).serialize());
+            } else if (path.endsWith("/same-site-check"_s)) {
+                sameSiteCheckHasCookie = contains(request.span(), "id=secret"_span);
+                co_await connection.awaitableSend(HTTPResponse("checked"_s).serialize());
+                receivedSameSiteCheck = true;
+            } else if (path.endsWith("/cross-site-check"_s)) {
+                crossSiteCheckHasCookie = contains(request.span(), "id=secret"_span);
+                co_await connection.awaitableSend(HTTPResponse("checked"_s).serialize());
+                receivedCrossSiteCheck = true;
+            }
+        }
+    });
+
+    RetainPtr storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+    [storeConfiguration setProxyConfiguration:@{
+        (NSString *)kCFStreamPropertyHTTPProxyHost: @"127.0.0.1",
+        (NSString *)kCFStreamPropertyHTTPProxyPort: @(server.port()),
+    }];
+    RetainPtr dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+    RetainPtr configuration = adoptNS([WKWebViewConfiguration new]);
+    [configuration setWebsiteDataStore:dataStore.get()];
+    RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://victim.example:%d/setcookie", server.port()]]]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://victim.example:%d/same-site-check", server.port()]]]];
+    Util::run(&receivedSameSiteCheck);
+    EXPECT_TRUE(sameSiteCheckHasCookie);
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://attacker.example:%d/attacker", server.port()]]]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://victim.example:%d/cross-site-check", server.port()]]]];
+    Util::run(&receivedCrossSiteCheck);
+    EXPECT_FALSE(crossSiteCheckHasCookie);
+}
Loading diff…

Original Bug Report

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