← WebKit — 2026-W36

95b954b3bd0f24cc26f5bd3548014fd6f1615327  WebContent processes can access back/forward items they don't own.

severity high class AuthorizationError confidence 0.85 signal score 9.42 Source/WTF/ patch-now AI review CONFIRMED
Primitive: WebContent process could name a back/forward item owned by another page via the process-global itemForID map
Triage note: SecurityFlags radar183491061 enforces that WebBackForwardList::itemForID only returns items whose pageID() matches m_page, and decidePolicyForNavigationAction now MESSAGE_CHECKs source/target BackForwardItemIdentifiers belong to the page. Previously items resolved against a process-global map with no ownership check, letting a compromised WebContent process reference other pages' items.
Structural signals: smartptr_member_added (+6.47), null_guard (+1.95), area:Source/WTF/ (+1.0)

The bug at a glance

A UIProcess authorization/isolation bug: WebBackForwardList::itemForID resolved a BackForwardItemIdentifier through the process-global WebBackForwardListItem::itemForID map with only a debug ASSERT that the item’s pageID matched, so in release builds a (compromised) WebContent process could name back/forward items owned by other pages. The fix (Source/WebKit/UIProcess/WebBackForwardList.{cpp,swift}, WebPageProxy.cpp, gated by SecurityFlags::radar183491061) rejects cross-page items and MESSAGE_CHECKs the source/target identifiers in decidePolicyForNavigationAction. This is a cross-page reference / isolation-boundary bypass, not memory corruption; it presupposes an already-compromised or misbehaving WebContent process, so it is a sandbox-hardening / second-stage defect. High within the process-isolation model; it is not independently RCE (the memory-safety consequences of acting on another page’s item are inferred).

The ownership invariant existed only as a debug ASSERT — release builds trusted whatever identifier the WebContent process sent and resolved it against a process-global map, so the UI process would operate on another page’s history item.

Root cause

BackForwardItemIdentifiers are minted in the UI process but referenced by WebContent over IPC; WebBackForwardListItem keeps a process-global itemForID(identifier) map shared across all pages in the process. The invariant the UI process must enforce is that a given page may only name items it owns (item->pageID() == m_page->identifier()).

Pre-patch sequence: 1. a WebContent process sends a navigation/back-forward IPC carrying a BackForwardItemIdentifier. 2. WebBackForwardList::itemForID looks it up in the process-global map and returns it, guarded only by ASSERT(…pageID() == m_page->identifier()) — compiled out in release. 3. WebPageProxy entry points (didCacheBackForwardItem, didEvictBackForwardItem, didTakeBackForwardItemForRestoration, discardBackForwardCacheEntry) resolve the identifier via the global map and act on it (touch its back/forward cache entry, evict, restore). 4. A compromised WebContent process supplies an identifier owned by another page and the UI process operates on that other page’s item across the isolation boundary.

The fix introduces SecurityFlags::radar183491061 and: in WebBackForwardList::itemForID (both the C++ and Swift implementations) resolves the item then returns nullptr if item->pageID() != m_page->identifier(); in WebPageProxy::decidePolicyForNavigationAction, MESSAGE_CHECK_COMPLETIONs that navigationActionData.sourceBackForwardItemIdentifier and targetBackForwardItemIdentifier belong to this page (a MESSAGE_CHECK kills a lying WebContent process); and routes the didCache/didEvict/didTake/discard entry points through a new backForwardItemForIdentifierFromWebContent() helper that resolves via the page’s own list (list.itemForID) instead of the raw global map.

Key code

itemForID now rejects items owned by another page instead of trusting the process-global map

auto* item = WebBackForwardListItem::itemForID(identifier);
if (!item)
    return nullptr;
// disable switch falls back to the process-global map with no ownership check
if (SecurityFlags::radar183491061() && item->pageID() != m_page->identifier())
    return nullptr;
return item;

Fix approach

The patch turns a debug-only ASSERT into an enforced ownership check behind the SecurityFlags::radar183491061 runtime flag. itemForID resolves the identifier and returns nullptr on a page-ID mismatch (identical logic in the C++ and the Swift UIProcess implementation, the latter gated with ENABLE_RC_REMOVE_SEC_ACCEL_3_FOR_SWIFT and using contentsMatch for the identifier comparison). WebContent-facing IPC handlers no longer call WebBackForwardListItem::itemForID(global) directly; they go through backForwardItemForIdentifierFromWebContent(list, itemID) which, when the flag is on, resolves via the page’s own WebBackForwardList so a foreign item never resolves. decidePolicyForNavigationAction gains two MESSAGE_CHECK_COMPLETIONs so a WebContent process that names a non-owned source/target item is treated as malicious (connection terminated) rather than silently served. The RC_REMOVE_SEC_ACCEL_3 (disable-switch) branch preserves the old global-map behavior for rollback.

Patch walkthrough

  • Source/WTF/Scripts/Preferences/SecurityFlags.yaml — Declares radar183491061 with a human-readable description: back/forward item access in the UI process may only come from the owning WebContent process.
  • Source/WTF/wtf/PlatformEnable.h — Adds ENABLE_RC_REMOVE_SEC_ACCEL_3_FOR_SWIFT so the Swift side can mirror the C++ #ifdef disable switch.
  • Source/WebKit/UIProcess/WebBackForwardList.cpp — itemForID resolves the item then returns nullptr when SecurityFlags::radar183491061() is set and item->pageID() != m_page->identifier(); includes SecurityFlags.h.
  • Source/WebKit/UIProcess/WebBackForwardList.swift — The Swift itemForID gains the same ownership guard (contentsMatch(item.pageID(), page.identifier())) under ENABLE_RC_REMOVE_SEC_ACCEL_3_FOR_SWIFT.
  • Source/WebKit/UIProcess/WebPageProxy.cpp — Adds isBackForwardItemIdentifierForPage() and MESSAGE_CHECK_COMPLETIONs on source/target identifiers in decidePolicyForNavigationAction; routes didCacheBackForwardItem / didEvictBackForwardItem / didTakeBackForwardItemForRestoration / discardBackForwardCacheEntry through backForwardItemForIdentifierFromWebContent() which resolves via the page’s own list.

Background

BackForwardItemIdentifier — A UI-process-minted identifier for a session-history entry; WebContent references it over IPC, so the UI process must validate ownership rather than trust the sender.

Process-global itemForID map — WebBackForwardListItem::itemForID is a static map keyed by identifier across all pages in the process; resolving against it without an ownership filter conflates items of different pages.

MESSAGE_CHECK / MESSAGE_CHECK_COMPLETION — WebKit IPC macros that validate an invariant on a message argument and terminate the sending WebContent process (optionally running a completion handler) if it fails — the standard response to a malformed/malicious IPC.

SecurityFlags runtime switch — A radar-numbered flag (radar183491061) that lets the enforcement be toggled; the RC_REMOVE_SEC_ACCEL_3 disable switch compiles the old unchecked path for emergency rollback.

Site isolation / compromised WebContent — The threat model here is a WebContent process already under attacker control trying to influence state (history) belonging to another page; UI-process checks are the trust boundary.

Vulnerability window

  1. Setup — Two pages exist in the same process-global item map; the attacker controls (or has compromised) one WebContent process.
  2. Craft — The compromised WebContent sends a navigation or back-forward-cache IPC carrying a BackForwardItemIdentifier belonging to the other page.
  3. Resolve — Pre-patch, the UI process resolves it via the global map with no ownership check (release ASSERT is a no-op) and operates on the foreign item.
  4. Impact — The UI process evicts/restores/caches another page’s history item, corrupting its navigation state or back-forward cache across the isolation boundary.

Proof of concept

No test ships in the diff and there is no pure-web trigger: exploitation requires a compromised WebContent process forging IPC. Conceptual trigger (inferred): from a controlled WebContent process, send decidePolicyForNavigationAction / didCacheBackForwardItem with a BackForwardItemIdentifier harvested for a different page; pre-patch the UI process resolves and acts on it, post-patch itemForID returns nullptr and the MESSAGE_CHECK terminates the process.

Exploitation

  1. Reach — Requires an already-compromised WebContent process (first-stage bug) able to craft UI-process IPC; this bug is a privilege/isolation escalation primitive, not an initial RCE.
  2. Trigger — Forge a navigation or back-forward-cache IPC naming another page’s item identifier.
  3. Primary primitive — Cross-page manipulation of session-history/back-forward-cache state in the UI process (evict, restore, cache a foreign item), breaking per-page isolation.
  4. Secondary — Acting on an item the current page does not own can drive the back-forward cache into an inconsistent state; whether this yields a UI-process memory-safety fault depends on downstream handling and is not shown by the diff (inferred).
  5. Open question — Whether cross-page item confusion can be steered into a UAF/type-confusion in the UI process (versus a logic/navigation-integrity violation) gates escalation beyond isolation bypass.

Detection & hunting

For defenders and detection engineers:

  • itemForID page-ID mismatch —
  • MESSAGE_CHECK termination on navigation —
  • IPC with foreign BackForwardItemIdentifier —

Remaining risk in the patch

  • Flag-gated enforcement — The ownership check is conditional on SecurityFlags::radar183491061(); if the flag is off (or the RC_REMOVE_SEC_ACCEL_3 disable switch is compiled), itemForID falls back to the unchecked global map, so the protection is only as strong as the flag’s default/rollout.
  • Coverage of all resolution sites — The fix routes four named IPC handlers plus decidePolicyForNavigationAction through the page-scoped resolver, but any other caller of WebBackForwardListItem::itemForID (global) that was not converted would retain the cross-page path — the diff does not show an exhaustive sweep (inferred).

Audit directions

  • All WebBackForwardListItem::itemForID callers —
  • Other process-global identifier maps —
  • Swift/C++ parity —

Before / after

Loading diff…