Released May 11, 2026

CVE-2026-28958

CVSS 5.5 severity medium CWE-200 vector CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

Overview

Impact:

Description: An app may be able to access sensitive user data

Researchers: Cantina

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.

Patch Details

diff --git a/Source/WebCore/loader/MixedContentChecker.cpp b/Source/WebCore/loader/MixedContentChecker.cpp
index bd42f3d7c2bd..f7c90310966e 100644
--- a/Source/WebCore/loader/MixedContentChecker.cpp
+++ b/Source/WebCore/loader/MixedContentChecker.cpp
@@ -131,10 +131,6 @@ bool MixedContentChecker::canModifyRequest(const URL& url, FetchOptions::Destina
 
 bool MixedContentChecker::shouldBlockRequest(Frame& frame, const URL& url, IsUpgradable isUpgradable)
 {
-    RefPtr<Document> document;
-    if (auto* localFrame = dynamicDowncast<LocalFrame>(frame))
-        document = localFrame->document();
-
 #if ENABLE(CONTENT_FILTERING) && HAVE(WEBCONTENTRESTRICTIONS)
     if (url == ContentFilter::blockedPageURL())
         return false;

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-28958
Bugzilla 311228
Component WebCore
Bug Class CrossOrigin
Severity high
Commit 093f34607a3e8c6b…
Advisory Apple Advisory

Terminology

  • 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.

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.

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.

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).

Changed Functions

Function File Change Note
FrameLoader::load Source/WebCore/loader/FrameLoader.cpp modified Computes Ref initiator = request.requester() and passes it to addSameSiteInfoToRequestIfNeeded (nullptr when the initiator inherits its origin from its owner), so SameSite classification uses the true initiating document instead of omitting it.

Files Changed

Source Files

  • Source/WebCore/loader/FrameLoader.cpp

Test Files

  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm

Regression Test

The fix ships this proof-of-concept / regression test:

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);
+}