CVE-2026-28958
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.
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).
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);
+}