d6c7e00d0a Block third-party cookies for requests from about:blank popups
Triage note: Closes a privacy/tracking bypass where about:blank popups inherited a context that skipped third-party cookie blocking.
Contents
The bug at a glance
This is a privacy / cross-origin cookie bug, not memory corruption: cross-origin requests made from an about:blank popup used firstPartyForCookies = about:blank, whose empty registrable domain caused thirdPartyCookieBlockingDecisionForRequest() to return None and skip third-party cookie blocking, leaking first-party cookies to third parties. It is rated medium because it defeats a privacy/anti-tracking control (ITP/resource-load-statistics third-party cookie blocking) rather than granting code execution, but it is trivially reachable from any page that can open a popup and enables cross-site tracking/cookie exfiltration. The fix restores correct first-party attribution by inheriting the opener’s firstPartyForCookies. The impact is confidentiality of cross-site cookie state, scoped to environments with third-party cookie blocking enabled.
An about:blank document has no URL-derived origin of its own — it inherits its security context from its opener — but FrameLoader::updateFirstPartyForCookies() blindly set firstPartyForCookies to the main frame URL (about:blank). Because about:blank has an empty registrable domain, the third-party cookie blocking decision short-circuits to None, so requests from the popup were treated as if there were no meaningful first party, bypassing blocking. The fix detects owner-inherited documents and inherits the opener’s firstPartyForCookies instead.
Root cause
firstPartyForCookies is the URL WebKit uses to decide whether a given request is first-party or third-party relative to the top-level browsing context; the resource-load-statistics machinery derives a registrable domain from it and, in thirdPartyCookieBlockingDecisionForRequest(), uses that domain to decide whether to block third-party cookies. The commit message states the key fact: for an empty registrable domain, thirdPartyCookieBlockingDecisionForRequest() returns ThirdPartyCookieBlockingDecision::None, i.e. no blocking is applied.
Before the patch, FrameLoader::updateFirstPartyForCookies() was a one-liner: if there is a page, setFirstPartyForCookies(page->mainFrameURL()). When the top-level document is an about:blank popup (window.open(‘about:blank’)), page->mainFrameURL() is about:blank. about:blank is an owner-inherited document — per the HTML/URL model it does not carry its own registrable domain; its effective origin is inherited from its opener/initiator. Setting firstPartyForCookies to about:blank therefore produced an empty registrable domain, and every cross-origin request the popup made (e.g. fetch(thirdPartyOrigin, {credentials:‘include’})) was evaluated with an empty first party. That empty domain drove thirdPartyCookieBlockingDecisionForRequest() to None, so third-party cookies that should have been blocked under resource-load-statistics were sent — the popup’s requests carried the third party’s own (first-party-set) cookies across contexts, defeating the block.
The fix rewrites updateFirstPartyForCookies() to compute firstPartyForCookies = page->mainFrameURL() and then, if SecurityPolicy::shouldInheritSecurityOriginFromOwner(firstPartyForCookies) is true (i.e. the URL is about:blank or another owner-inherited scheme), inherit from the opener: it downcasts page->mainFrame().opener() to a LocalFrame, and if that opener has a document, uses openerDocument->firstPartyForCookies() as the value. Only then does it call setFirstPartyForCookies(firstPartyForCookies). This makes the popup’s first party match the opener’s real first party, so a cross-origin request from the about:blank popup is correctly classified as third-party and thirdPartyCookieBlockingDecisionForRequest() applies blocking as it would from the opener itself.
The guard SecurityPolicy::shouldInheritSecurityOriginFromOwner is the same predicate WebKit uses elsewhere to decide when a document inherits its opener/parent origin (about:blank, about:srcdoc, javascript:, data: depending on context), so the cookie first-party now tracks the same inheritance rule as the security origin. The dynamicDowncast<LocalFrame> and null checks make the change a no-op when there is no local opener (e.g. the opener is a remote frame under site isolation, or was closed), falling back to the original mainFrameURL value.
Key code
updateFirstPartyForCookies inherits the opener’s first party for about:blank popups (FrameLoader.cpp)
void FrameLoader::updateFirstPartyForCookies()
{
RefPtr page = m_frame->page();
if (!page)
return;
auto firstPartyForCookies = page->mainFrameURL();
if (SecurityPolicy::shouldInheritSecurityOriginFromOwner(firstPartyForCookies)) {
if (RefPtr opener = dynamicDowncast<LocalFrame>(page->mainFrame().opener())) {
if (RefPtr openerDocument = opener->document())
firstPartyForCookies = openerDocument->firstPartyForCookies();
}
}
setFirstPartyForCookies(firstPartyForCookies);
}
Patch walkthrough
Source/WebCore/loader/FrameLoader.cpp— Rewrites FrameLoader::updateFirstPartyForCookies(). Instead of unconditionally setting firstPartyForCookies to page->mainFrameURL(), it checks SecurityPolicy::shouldInheritSecurityOriginFromOwner(firstPartyForCookies); if so (about:blank etc.), it inherits openerDocument->firstPartyForCookies() from the local opener frame. This gives the about:blank popup a real first party so third-party cookie blocking is applied.LayoutTests/http/tests/resourceLoadStatistics/third-party-cookie-blocking-about-blank-popup.html— New layout test. Sets a cookie on a third-party origin, enables statistics-based third-party cookie blocking, opens an about:blank popup, and has the popup fetch the third-party echo-cookies endpoint with credentials; it passes only if the cookie is absent (blocked).LayoutTests/http/tests/resourceLoadStatistics/third-party-cookie-blocking-about-blank-popup-expected.txt— Expected output asserting ‘PASS about:blank popup: third-party cookie was correctly blocked.’
Background
firstPartyForCookies — firstPartyForCookies is the URL representing the top-level browsing context that a network request belongs to. WebKit compares a request’s origin against this URL’s registrable domain to classify the request as first-party or third-party, which in turn drives cookie policy including third-party cookie blocking. If it is wrong (empty or pointing at the wrong site) the entire first/third-party classification for that context is wrong.
about:blank inheriting opener origin — An about:blank document created by window.open or an iframe has no URL-derived origin; per the HTML standard it inherits its security origin from the browsing context that created it (its opener/initiator). SecurityPolicy::shouldInheritSecurityOriginFromOwner encodes this for about:blank, about:srcdoc, and similar owner-inherited URLs. The bug was that cookie first-party attribution did not follow the same inheritance rule as the security origin.
Registrable domain and empty-domain short-circuit — Third-party cookie blocking is decided on registrable domains (eTLD+1). about:blank has no host and thus an empty registrable domain. thirdPartyCookieBlockingDecisionForRequest() treats an empty registrable domain as ’no meaningful first party’ and returns ThirdPartyCookieBlockingDecision::None, i.e. it does not block — so an about:blank first party silently disables blocking for that context.
Resource Load Statistics / ITP third-party cookie blocking — WebKit’s Intelligent Tracking Prevention (resource-load-statistics) can block cookies on cross-site (third-party) requests to limit tracking. When enabled, requests classified as third-party do not carry the destination’s first-party cookies. A bug that mis-classifies a third-party request as having no first party defeats this protection and leaks cross-site cookies.
Opener chain under site isolation — page->mainFrame().opener() returns the frame that opened this window. The fix downcasts it to LocalFrame with dynamicDowncast; under site isolation the opener may be a RemoteFrame in another process, in which case the downcast fails and the code falls back to the raw mainFrameURL. This keeps the change safe but means the inheritance path applies when the opener is in-process.
Vulnerability window
- Baseline — updateFirstPartyForCookies() sets firstPartyForCookies to page->mainFrameURL() unconditionally.
- Popup creation — A page opens window.open(‘about:blank’); the popup’s main frame URL is about:blank with an empty registrable domain.
- Leak — The popup issues credentialed cross-origin requests; thirdPartyCookieBlockingDecisionForRequest() returns None due to the empty domain, so third-party cookies are sent despite blocking being enabled.
- Fix — updateFirstPartyForCookies() now inherits the opener’s firstPartyForCookies for owner-inherited URLs, restoring correct third-party classification.
- Test — Adds a resource-load-statistics layout test verifying the third-party cookie is blocked from the about:blank popup.
- Release — Landed 313637@main on May 20 2026 (bug 308445 / rdar://168927271), originally on a safari-7624 branch as 305413.366; reviewed by Matthew Finkel.
Proof of concept
Distilled from the verbatim added test third-party-cookie-blocking-about-blank-popup.html. With statistics-based third-party cookie blocking enabled, the opener sets a cookie on a third-party origin, opens an about:blank popup, and has the popup fetch the third-party echo-cookies endpoint with credentials:‘include’. On the pre-patch build the third-party cookie is sent because the popup’s first party is about:blank (empty domain -> None); post-patch it is blocked.
// From the added layout test (step2): open an about:blank popup and
// make a credentialed cross-origin fetch from inside it.
var popup = window.open("about:blank");
popup.eval(
"fetch('" + thirdPartyOrigin + "/cookies/resources/echo-cookies.py', { credentials: 'include' })" +
".then(function(r) { return r.text(); })" +
".then(function(text) {" +
" window.opener.postMessage({ type: 'fetch-done', result: text }, '*');" +
"});"
);
// Pre-patch: echo-cookies response contains aboutBlankTestCookie (leaked).
// Post-patch: cookie is blocked and absent from the response.
Exploitation
- Reach — Any page that can call window.open(‘about:blank’) and run script in the popup (same-process opener). No special privileges required.
- Bypass — The about:blank first party yields an empty registrable domain, so thirdPartyCookieBlockingDecisionForRequest() returns None and third-party cookie blocking is skipped for the popup’s cross-origin requests.
- Exfiltrate — The popup issues credentialed cross-origin requests to a third party; the third party’s cookies ride along, and results are relayed back to the opener via postMessage, enabling cross-site tracking / cookie linkage.
- Outcome — Privacy/tracking bypass and cross-site cookie leakage in environments with ITP third-party cookie blocking enabled; no memory-safety impact.
Detection & hunting
For defenders and SOC / detection engineers:
- Credentialed cross-origin fetch from about:blank popups —
- window.open(‘about:blank’) + immediate cross-site credentialed request —
- Cookie leakage regression tests —
Audit directions
- Other owner-inherited document types —
- Empty-registrable-domain short-circuits —
- Remote opener under site isolation —
- firstPartyForCookies vs securityOrigin parity —