← WebKit Silent-Fix Report — 2026-W22

888c80c640  [Site Isolation] CSP upgrade-insecure-requests not applied when cross-process iframe navigates top frame

severity medium class Bypass confidence 0.70 WebCore CSP site-isolation exploitable-grade
Roberto Rodriguez Sat May 30 12:21:47 2026 -0700 full: 888c80c640da3d5a5224f727e3e45fbd6fbdf5b8 bug report ↗ view on GitHub ↗
Primitive: upgrade-insecure-requests not applied cross-process
Triage note: Propagates upgrade-insecure-requests origins across processes so navigations are upgraded, closing a mixed-content/CSP bypass under site isolation.
Contents

The bug at a glance

A CSP upgrade-insecure-requests / mixed-content bypass specific to site isolation: when a cross-origin iframe navigates window.top.location.href to an http:// URL, the top frame’s upgrade-insecure-requests directive was never applied because the site-isolation path routes through a provisional page in another process where no document exists to consult, so the navigation proceeded over cleartext. It downgrades a security guarantee the author explicitly opted into (silent http instead of forced https), exposing the top-level navigation to network MITM/downgrade. It is not memory corruption and requires a cross-process navigation configuration, so medium, but it is a genuine same-page security-policy bypass.

upgrade-insecure-requests is normally enforced in FrameLoader::changeLocation() using the target frame’s live document CSP. Under site isolation, a cross-process navigation of the top frame bypasses that code entirely and lands in a provisional page in the UIProcess where the target document does not yet exist – so there is nothing to check and the upgrade is silently skipped. The fix mirrors each frame’s set of CSP upgrade origins into its WebFrameProxy in the UIProcess and re-applies the upgrade in receivedNavigationActionPolicyDecision before process routing.

Root cause

The upgrade-insecure-requests CSP directive tells the browser to transparently rewrite http:// subresource and same-origin navigation requests to https:// for a set of origins. In the single-process case this is enforced in WebCore’s FrameLoader::changeLocation() against the navigating frame’s document ContentSecurityPolicy, which holds m_insecureNavigationRequestsToUpgrade (a HashSet<SecurityOriginData>).

Under site isolation, when a cross-origin iframe navigates the top frame (window.top.location.href = ‘http://…’), the navigation is handed to the UIProcess and executed as a provisional page in a (possibly different) process. That path does not run the WebCore document-CSP upgrade – at decision time there is no committed target document whose CSP can be consulted – so the top frame’s upgrade-insecure-requests set is never applied and the load proceeds over http.

The patch plumbs the upgrade origins to the UIProcess so the decision can be made there:

  1. WebCore: ContentSecurityPolicy gains notifyInsecureNavigationRequestsToUpgradeChanged(), invoked from setUpgradeInsecureRequests, inheritInsecureNavigationRequestsToUpgradeFromOpener, and setInsecureNavigationRequestsToUpgrade. When site isolation is enabled and the CSP belongs to a framed document, it calls the frame loader client’s new dispatchDidChangeCSPOriginsThatUpgradeInsecureNavigations(m_insecureNavigationRequestsToUpgrade). A public accessor insecureNavigationRequestsToUpgrade() is added.

  2. IPC: WebLocalFrameLoaderClient::dispatchDidChangeCSPOriginsThatUpgradeInsecureNavigations sends Messages::WebFrameProxy::DidChangeCSPOriginsThatUpgradeInsecureNavigations (new, [EnabledBy=SiteIsolationEnabled]) for dynamically-added directives (meta tag). For directives arriving via response header, the set is instead carried inside DidCommitLoadForFrame, whose signature/message now includes HashSet<WebCore::SecurityOriginData> cspOriginsThatUpgradeInsecureNavigations (threaded through ProvisionalPageProxy, WebPageProxy::commitProvisionalPage/didCommitLoadForFrame, WebFrameProxy::commitProvisionalFrame/didCommitLoad).

  3. UIProcess state: WebFrameProxy stores m_cspOriginsThatUpgradeInsecureNavigations with get/set accessors and a didChangeCSPOriginsThatUpgradeInsecureNavigations handler; didCommitLoad records the set from the committing document.

  4. Enforcement: in WebPageProxy::receivedNavigationActionPolicyDecision, before computing the Site and routing the navigation, if the navigation is cross-process (&processInitiatingNavigation != &frame.process()) and frame.cspOriginsThatUpgradeInsecureNavigations() contains SecurityOriginData::fromURL(navigation.currentRequest().url()), it calls navigation.upgradeCurrentInsecureRequest(). API::Navigation::upgradeCurrentInsecureRequest() rewrites m_currentRequest’s URL via ResourceRequestBase::upgradeInsecureRequest (now WEBCORE_EXPORT). Same-process navigations are untouched because the WebProcess already upgraded them.

The LayoutTest expectation change removes upgrade-insecure-requests/link-upgrade.sub.https.html from the mac-site-isolation failure list, confirming the previously-failing (unupgraded) behavior is now fixed.

Key code

Apply the frame’s CSP upgrade set before process routing for cross-process navigations (WebPageProxy::receivedNavigationActionPolicyDecision)

    // Apply CSP upgrade-insecure-requests before computing Site. Only needed for remote-frame
    // navigations. Same-process navigations are already upgraded in the WebProcess.
    if (&processInitiatingNavigation != &frame.process()) {
        auto& upgradeSet = frame.cspOriginsThatUpgradeInsecureNavigations();
        if (upgradeSet.contains(WebCore::SecurityOriginData::fromURL(navigation.currentRequest().url())))
            navigation.upgradeCurrentInsecureRequest();
    }

Patch walkthrough

  • Source/WebCore/page/csp/ContentSecurityPolicy.cpp — Adds notifyInsecureNavigationRequestsToUpgradeChanged() and calls it from setUpgradeInsecureRequests, inheritInsecureNavigationRequestsToUpgradeFromOpener, and setInsecureNavigationRequestsToUpgrade. The notifier, when siteIsolationEnabled() and the CSP’s context is a framed Document, calls frame->loader().client().dispatchDidChangeCSPOriginsThatUpgradeInsecureNavigations(m_insecureNavigationRequestsToUpgrade). Adds includes for FrameLoader.h and LocalFrameLoaderClient.h.
  • Source/WebCore/page/csp/ContentSecurityPolicy.h — Adds the public accessor insecureNavigationRequestsToUpgrade() and declares the private notifyInsecureNavigationRequestsToUpgradeChanged() const.
  • Source/WebCore/loader/LocalFrameLoaderClient.h — Adds virtual dispatchDidChangeCSPOriginsThatUpgradeInsecureNavigations(const HashSet<SecurityOriginData>&) hook (default empty).
  • Source/WebCore/platform/network/ResourceRequestBase.h — Marks static upgradeInsecureRequest(URL&) WEBCORE_EXPORT so the UIProcess (WebKit) can call it from API::Navigation.
  • Source/WebKit/UIProcess/API/APINavigation.cpp — Adds Navigation::upgradeCurrentInsecureRequest(): takes the current request URL, applies ResourceRequestBase::upgradeInsecureRequest, and stores it back via m_currentRequest.setURL.
  • Source/WebKit/UIProcess/WebPageProxy.cpp — receivedNavigationActionPolicyDecision gains the enforcement block: for cross-process navigations (&processInitiatingNavigation != &frame.process()), if frame.cspOriginsThatUpgradeInsecureNavigations() contains the request URL’s origin, it calls navigation.upgradeCurrentInsecureRequest() before Site computation/routing. commitProvisionalPage and didCommitLoadForFrame are threaded with the new cspOriginsThatUpgradeInsecureNavigations argument.
  • Source/WebKit/UIProcess/WebFrameProxy.cpp / .h — WebFrameProxy stores m_cspOriginsThatUpgradeInsecureNavigations with cspOriginsThatUpgradeInsecureNavigations()/setCSPOriginsThatUpgradeInsecureNavigations() accessors; didCommitLoad records it; new didChangeCSPOriginsThatUpgradeInsecureNavigations() (IPC target) updates it for dynamically-added directives. commitProvisionalFrame is threaded with the new argument. Adds SecurityOriginData.h include.
  • Source/WebKit/UIProcess/WebFrameProxy.messages.in — Adds [EnabledBy=SiteIsolationEnabled] DidChangeCSPOriginsThatUpgradeInsecureNavigations(HashSet<WebCore::SecurityOriginData>) message so the WebProcess can push dynamically-added meta-tag upgrade origins to the frame proxy.
  • Source/WebKit/UIProcess/WebPageProxy.messages.in — Extends DidCommitLoadForFrame with a HashSet<WebCore::SecurityOriginData> cspOriginsThatUpgradeInsecureNavigations field so header-delivered upgrade origins ride along with the commit.
  • Source/WebKit/WebProcess/WebCoreSupport/WebLocalFrameLoaderClient.cpp / .h — Implements dispatchDidChangeCSPOriginsThatUpgradeInsecureNavigations (guarded by siteIsolationEnabled()) sending the WebFrameProxy message, and updates dispatchDidCommitLoad to include the committing document’s contentSecurityPolicy().insecureNavigationRequestsToUpgrade() in the DidCommitLoadForFrame send.
  • Source/WebKit/UIProcess/ProvisionalPageProxy.cpp / .h — didCommitLoadForFrame signature is extended with cspOriginsThatUpgradeInsecureNavigations and forwards it into page->commitProvisionalPage.
  • LayoutTests/platform/mac-site-isolation/TestExpectations — Removes upgrade-insecure-requests/link-upgrade.sub.https.html from the site-isolation Failure list, reflecting that the upgrade is now applied.

Background

upgrade-insecure-requests — A CSP directive that instructs the UA to transparently rewrite http:// requests (subresources and same-origin navigations) to https:// for a declared set of origins, so authors can migrate to HTTPS without mixed-content breakage.

m_insecureNavigationRequestsToUpgrade — The ContentSecurityPolicy member (HashSet<SecurityOriginData>) recording which origins’ navigations must be upgraded. It is the authoritative set this patch mirrors to the UIProcess.

Site isolation provisional page — A cross-process navigation is staged as a provisional page/frame in a target process and committed via DidCommitLoadForFrame. At policy-decision time no target document exists in that process, so document-CSP-based upgrades cannot run there.

FrameLoader::changeLocation() — The WebCore path that normally applies upgrade-insecure-requests using the target frame’s document CSP. The site-isolation top-frame navigation bypasses it, which is the root of the bypass.

API::Navigation / currentRequest — The UIProcess object modeling an in-flight navigation. The new upgradeCurrentInsecureRequest() rewrites its currentRequest URL to https via ResourceRequestBase::upgradeInsecureRequest before the process/Site is chosen.

DidChangeCSPOriginsThatUpgradeInsecureNavigations IPC — New [EnabledBy=SiteIsolationEnabled] WebFrameProxy message that carries dynamically-added (meta-tag) upgrade origins to the UIProcess; header-delivered origins instead ride along DidCommitLoadForFrame.

Vulnerability window

  1. Author policy — A top-level document sets Content-Security-Policy: upgrade-insecure-requests, populating its CSP m_insecureNavigationRequestsToUpgrade.
  2. Cross-process navigation — A cross-origin iframe (isolated into another process) sets window.top.location.href to an http:// URL, initiating a cross-process navigation of the top frame.
  3. Bypass — The navigation is staged as a provisional page in the UIProcess/target process; FrameLoader::changeLocation’s document-CSP upgrade never runs, so the load proceeds over cleartext http.
  4. Plumbing — Post-patch, each frame’s CSP upgrade origins are synced to its WebFrameProxy via DidCommitLoadForFrame (header) or DidChangeCSPOriginsThatUpgradeInsecureNavigations (meta).
  5. Enforcement — receivedNavigationActionPolicyDecision consults frame.cspOriginsThatUpgradeInsecureNavigations() and, for cross-process navigations, calls navigation.upgradeCurrentInsecureRequest() to rewrite the URL to https before routing.
  6. Verification — The mac-site-isolation TestExpectations entry for upgrade-insecure-requests/link-upgrade.sub.https.html is removed, confirming the fix.

Triggering

No new test file in the diff; the fix instead un-marks the existing WPT imported/w3c/web-platform-tests/upgrade-insecure-requests/link-upgrade.sub.https.html as failing under mac-site-isolation. Conceptual trigger: with site isolation enabled, load a top document sending upgrade-insecure-requests, embed a cross-origin iframe isolated into another process, and from that iframe set window.top.location.href = ‘http://<same-or-listed-origin>/…’; pre-patch the top navigation stays on http (bypass), post-patch it is upgraded to https before process routing.

Exploitation

  1. Setup — Attacker controls (or can script) a cross-origin iframe embedded in a site that relies on upgrade-insecure-requests, under a site-isolation configuration.
  2. Trigger downgrade — From the iframe, navigate the top frame to an http:// URL; the site-isolation path skips the document-CSP upgrade, leaving the top-level load on cleartext.
  3. MITM — A network attacker on the path can now intercept/modify the unupgraded top-level http navigation and response that the author intended to be forced to https.
  4. Impact — Effectively a same-origin security-policy downgrade: content injection / credential exposure over http for a site that opted into HTTPS-only navigation, without any memory-safety component.

Detection & hunting

For defenders and SOC / detection engineers:

  • Top-frame http navigation despite upgrade-insecure-requests under site isolation — Detect cross-process top-frame navigations that land on http for origins present in the frame’s upgrade set – the observable bypass. Network logs plus CSP headers reveal it.
  • WebFrameProxy upgrade set empty at policy decision — Instrument receivedNavigationActionPolicyDecision to log when a cross-process navigation targets an origin the top document’s CSP should upgrade but the frame proxy’s set is empty (pre-patch state).
  • Missing DidChangeCSPOriginsThatUpgradeInsecureNavigations sync — Verify meta-tag-added upgrade-insecure-requests directives propagate to the UIProcess; absence indicates dynamically-added policies are not being enforced cross-process.

Audit directions

  • Other CSP/security directives lost on cross-process navigation — Audit whether block-all-mixed-content, sandbox, referrer policy, and other document-CSP-enforced navigation constraints are similarly bypassed when a cross-process navigation stages a provisional page with no target document; each may need analogous UIProcess plumbing.
  • Consistency of the upgrade set across processes — Verify m_cspOriginsThatUpgradeInsecureNavigations is populated for every path that establishes a frame (header commit, meta dynamic, opener inheritance, back/forward, initial about:blank) so no navigation slips through with a stale/empty set.
  • receivedNavigationActionPolicyDecision URL vs redirects — Confirm the upgrade is applied against the correct final URL (post-redirect, currentRequest) and that ordering relative to Site computation, enhanced-security tracking, and process selection is correct so an unupgraded URL never drives process routing.
  • SecurityOriginData::fromURL matching semantics — Check that origin matching in the upgrade set (fromURL of the navigation URL vs stored origins, ws/wss handling, ports) matches WebCore’s in-process upgrade logic so cross-process behavior does not diverge (over- or under-upgrading).

Before / after

Loading diff…