← WebKit — 2026-W35

81a9b86c9de1dfe07c9330e94b2bbed5e00fbf9f  Missing same-site check on targetBackForwardItemIdentifier in WebProcessPool::processForNavigationInternal

severity high class PolicyBypass confidence 0.68 signal score 6.17 Source/WebKit/UIProcess patch-now AI review CONFIRMED
Primitive: cross-site back/forward navigation reuses the target item's suspended-page process, granting it another site's first-party privileges
Triage note: WebProcessPool::processForNavigation now only lets navigation.targetItem() influence process selection when Site{targetItem->url()}==site/targetSite; the comment states otherwise 'the targeted process might get some undeserved first party privileges'. A process/site-isolation policy bug reachable via history navigation.
Structural signals: smartptr_member_added (+4.87), surgical (+1.3)

The bug at a glance

A site-isolation process-selection policy bypass in WebProcessPool::processForNavigation (Source/WebKit/UIProcess/WebProcessPool.cpp), a UIProcess logic flaw with no memory corruption. On a main-frame back/forward navigation, the code let the target history item’s suspended-page process be reused for process selection without checking that the target item’s site matched the site being navigated to, so a cross-site back/forward could land in a process belonging to a different site and inherit that site’s first-party privileges (the in-tree comment: ’the targeted process might get some undeserved first party privileges’). The diff clearly proves the policy gap and the fix; high severity is reasonable because it weakens the site-isolation boundary (cross-site privilege/state confusion), but it is a logic/PolicyBypass bug — not a crash or RCE — so impact is scoped to the security-boundary violation it enables (inferred as a stepping stone, not direct code execution).

Process reuse driven by the back/forward item instead of the destination site: a suspended page from one site could be adopted for a navigation to another, quietly granting the wrong origin’s first-party context.

Root cause

The invariant under site isolation is that a main-frame navigation to site S must run in a process legitimately associated with S, so first-party privileges (cookies/storage/credential context) are not handed to another site. processForNavigation, when choosing a process for a back/forward navigation, consulted navigation.targetItem() (the destination history item) and, if that item owned a suspendedPage, reused that suspended page’s process. Pre-patch the only guards were targetItem && frame.isMainFrame() (site-isolation branch) and targetItem && frame.isMainFrame() && !siteIsolationEnabled (the general branch) — neither verified that the target item’s URL belonged to the site being navigated to.

So the pre-patch sequence is: 1. a main-frame back/forward navigation produces a navigation whose targetItem points at a history entry for site A that still has a live suspendedPage (process P_A); 2. the navigation’s actual destination resolves to site B (site/targetSite); 3. processForNavigation sees targetItem->suspendedPage(), confirms its process is not Terminated, and selects P_A for the navigation; 4. the page for site B is now hosted in a process that was set up for site A, so it inherits A’s first-party privileges. The fix adds && Site { URL { targetItem->url() } } == site (and == targetSite in the general branch) so the target item can only steer process selection when its own site equals the destination site.

A subtle secondary point is in the comment: the code deliberately builds two Site objects and compares them with operator== rather than using Site::matches(), because matches() derives the registrable domain from URL::host(), which is empty for a blob: URL — so a blob-URL history item would fail to match even itself, incorrectly suppressing legitimate same-document reuse. Bugclass matches the triage (PolicyBypass); this is a logic/authorization defect, not memory safety.

Key code

Target back/forward item may steer process selection only when its site equals the destination site

-    if (RefPtr targetItem = navigation.targetItem(); targetItem && frame.isMainFrame() && !siteIsolationEnabled) {
+    // Only allow the target back/forward item to influence process selection when the navigation is within the same site.
+    // Otherwise the targeted process might get some undeserved first party privileges.
+    // Compare two Sites rather than using Site::matches(): matches() derives the registrable domain from
+    // URL::host(), which is empty for a blob URL, so a blob-URL item would not even match itself.
+    if (RefPtr targetItem = navigation.targetItem(); targetItem && frame.isMainFrame() && !siteIsolationEnabled && Site { URL { targetItem->url() } } == targetSite) {

Fix approach

The patch gates target-item-driven process reuse on a same-site check in both affected branches: the site-isolation branch now requires Site { URL { targetItem->url() } } == site, and the general branch requires == targetSite, before a targetItem (and its suspendedPage’s process) is allowed to influence which process the navigation lands in. The comparison is done by constructing two Site values and using operator== rather than Site::matches(), specifically so blob: URLs (whose URL::host() is empty and which therefore misbehave under matches()) compare correctly. The semantic change: a cross-site back/forward can no longer adopt the destination history item’s suspended-page process; such navigations fall through to normal site-appropriate process selection, closing the undeserved-first-party-privilege path while preserving legitimate same-site suspended-page reuse (including for blob-URL items).

Patch walkthrough

  • Source/WebKit/UIProcess/WebProcessPool.cpp — In both the siteIsolationEnabled path and the general processForNavigation path, adds a Site{URL{targetItem->url()}} == site / == targetSite condition before letting the target back/forward item’s suspended-page process drive process selection, with comments explaining the first-party-privilege risk and the deliberate choice of Site operator== over Site::matches() to handle empty-host blob URLs.

Background

Site isolation / process-per-site — WebKit’s security architecture assigns web content to processes by site so one site’s content cannot access another’s in-process. Process-selection logic that picks the wrong process undermines this boundary directly, without any memory bug.

Back/forward item & suspended page — A history (back/forward) item can retain a SuspendedPageProxy — a cached, suspended web process for that entry — so navigating back to it is fast. Reusing that process is only safe when the entry’s site matches where you’re navigating.

Site vs Site::matches() — Site is WebKit’s security principal (scheme + registrable domain). Site::matches() derives the registrable domain from URL::host(); for a blob: URL host() is empty, so matches() fails even for identical blob items — hence the fix compares two constructed Site objects with operator==.

First-party privileges — The ambient authority a process holds as a given site: access to that site’s cookies, storage, and credentialed context. Hosting site B’s page in site A’s process leaks A’s first-party context to B’s content (and vice versa).

processForNavigation — The UIProcess decision point that chooses which web process (new, reused source, or a suspended-page process) will service a navigation; its guards are the enforcement of site isolation for history navigations.

Vulnerability window

  1. Setup — A tab builds a history across sites so a back/forward item for site A retains a live suspended-page process P_A.
  2. Trigger — The attacker drives a main-frame back/forward navigation whose destination resolves to a different site B while targetItem still references the site-A entry.
  3. Mis-selection — Pre-patch processForNavigation reuses targetItem->suspendedPage()’s process P_A (non-terminated) for the navigation to B.
  4. Boundary violation — Site B’s content runs in P_A, inheriting site A’s first-party privileges (cross-site cookie/storage/context confusion).
  5. Abuse (inferred) — The mis-attributed first-party context is leveraged as a cross-site data/privilege bridge; concrete exploitation beyond the boundary break is inferred.

Proof of concept

No test ships in the diff (conceptual; UIProcess policy, web-driven). Conceptual trigger: navigate a tab A->B across sites so the back/forward list holds a site-A entry with a retained suspended page, then issue a main-frame history navigation (history.back/forward or location change) whose target item is the site-A entry but whose resolved destination is a different site (e.g. via a blob: URL item, which also exercises the Site::matches() empty-host quirk the comment calls out). Pre-patch the navigation reuses the site-A suspended-page process. Observing the privilege leak requires UIProcess process-assignment instrumentation; exact steps are inferred.

Exploitation

  1. Reach — Web-driven history navigation; requires building a cross-site back/forward history with a surviving suspended page, no special permissions.
  2. Trigger — A main-frame back/forward whose target item’s site differs from the destination site (blob: URL items especially, per the matches() quirk).
  3. Primary primitive — Process mis-selection: destination content hosted in another site’s process, i.e. a site-isolation boundary break granting undeserved first-party privileges.
  4. Secondary — Within the wrong process, the page can reach that site’s in-process first-party state (cookies/storage/credentialed context), enabling cross-site data access or state confusion.
  5. Escalation (open) — Open questions gating real-world impact: how reliably an attacker forces the destination-vs-target site mismatch while a suspended page survives, and what concrete first-party resources become reachable in the mis-assigned process. No memory-corruption primitive is implied by the diff (inferred).

Detection & hunting

For defenders and detection engineers:

  • Suspended-page reuse across differing sites —
  • Blob-URL history items in back/forward reuse —
  • Cross-site process assignment audit —

Remaining risk in the patch

  • Comparison correctness for non-standard URLs — The fix hinges on Site{URL{targetItem->url()}} comparing correctly; URL schemes with empty or unusual hosts (blob:, data:, about:, file:) are exactly where Site semantics are fragile, and the comment shows this class of quirk already caused one bug — other such URLs are unverified from the diff.
  • Scope limited to two branches — Only the two processForNavigation branches shown are gated; if any other process-selection path (not in this diff) consults targetItem without the same-site check, the undeserved-privilege window remains open there.

Audit directions

  • Other targetItem-driven decisions —
  • Site::matches() vs operator== call sites —
  • Suspended-page process lifetime —
  • First-party privilege provenance —

Before / after

Loading diff…