← All issues

[4] FrameLoader inherits empty registrable domain for about:blank popups

Severity: Medium | Component: WebCore FrameLoader | d6c7e00

Rated Medium because the diff fixes a reliable third-party-cookie-blocking bypass reachable from any first-party page via window.open('about:blank'); impact is privacy/tracking, not memory safety.

When an about:blank popup made cross-origin requests, its firstPartyForCookies was set to about:blank, which has an empty registrable domain. thirdPartyCookieBlockingDecisionForRequest() returns ThirdPartyCookieBlockingDecision::None for empty domains, bypassing third-party cookie blocking. FrameLoader::updateFirstPartyForCookies() should inherit the opener's firstPartyForCookies when the main frame URL is an owner-inherited document.

Source/WebCore/loader/FrameLoader.cpp

void FrameLoader::updateFirstPartyForCookies()
{
- if (RefPtr page = m_frame->page())
- setFirstPartyForCookies(page->mainFrameURL());
+ 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);
}

LayoutTests/http/tests/resourceLoadStatistics/third-party-cookie-blocking-about-blank-popup.html

+ case "#step2":
+ var popup = window.open("about:blank");
+ popup.eval(
+ "fetch('" + thirdPartyOrigin + "/cookies/resources/echo-cookies.py', { credentials: 'include' })" + ...
+ );

updateFirstPartyForCookies() previously set the frame's firstPartyForCookies to page->mainFrameURL() unconditionally. The fix adds a check: if the main frame URL is one that should inherit its security origin from its owner (via SecurityPolicy::shouldInheritSecurityOriginFromOwner()), it walks to page->mainFrame().opener() (downcast to LocalFrame), retrieves the opener document's firstPartyForCookies(), and uses that instead.

Failure to mirror opener/parent inheritance into the first-party-for-cookies field when the document URL is an owner-inherited scheme like about:blank.

firstPartyForCookies is a per-frame URL recorded on outgoing requests; the network layer uses its registrable domain (eTLD+1) as the "first party" when deciding whether a cookie destination is first- or third-party. For about:blank and other opaque schemes, the registrable domain is the empty string. WebKit's third-party cookie blocking (Intelligent Tracking Prevention) suppresses cookies on cross-site subresource requests when the destination has been classified as a tracker; the decision is gated on thirdPartyCookieBlockingDecisionForRequest(). Per HTML spec, an about:blank document inherits its security origin from the browsing context that created it; SecurityPolicy::shouldInheritSecurityOriginFromOwner(url) returns true for the set of URL schemes (about:blank, about:srcdoc, data: etc.) that follow this rule.

When a page opened an about:blank popup, the popup's main frame URL was literally about:blank. updateFirstPartyForCookies() then set firstPartyForCookies on the popup's frame to that URL. When the popup subsequently issued a cross-origin request, the network layer computed the registrable domain of about:blank — empty. thirdPartyCookieBlockingDecisionForRequest() treats an empty registrable domain as None, i.e. "do not block", because there is no meaningful first party to compare against.

From a first-party page A, call window.open('about:blank') to obtain a popup whose main frame URL is about:blank. Inside the popup, issue credentialed cross-origin requests, e.g. fetch('https://tracker.example/...', {credentials: 'include'}). Pre-fix, the popup's firstPartyForCookies is about:blank, its registrable domain is empty, and any previously-set third-party cookies for tracker.example are attached to the request — exactly the case the regression test demonstrates with cookieName = 'aboutBlankTestCookie'.

The fix only handles the main-frame opener chain via LocalFrame; if the opener is a RemoteFrame (site-isolation case), the dynamicDowncast<LocalFrame> fails silently and the original (empty-registrable-domain) URL is still used. Worth flagging for follow-up.

This vulnerability weakened the third-party cookie blocking trust boundary that ITP and resource-load-statistics rely on. The security model assumes that every top-level browsing context has a stable, non-empty first-party registrable domain; before the fix this invariant was violated for any popup whose initial document URL was owner-inherited. First-party-for-cookies is a parallel state to security origin, and any time a URL scheme has special origin-inheritance rules (about:blank, about:srcdoc, data:, sandboxed iframes, javascript: URLs) the cookie-policy first party must mirror the same inheritance — otherwise the network layer silently downgrades to "no first party".