23b15df9eb didSameDocumentNavigationForFrame accepts arbitrary URL, enabling address bar spoofing
Triage note: Adds IPC validation preventing a web process from spoofing the displayed URL via same-document navigation.
Contents
The bug at a glance
A compromised or malicious web content process could send the UI process a didSameDocumentNavigationForFrameViaJS IPC carrying an arbitrary URL, which the UI process would accept and reflect as the frame’s committed location. Because the browser chrome (address bar) reads the UI-process notion of the frame URL, this let script display an attacker-chosen origin — e.g. a bank’s https URL — while the page content stayed under attacker control, a high-impact address-bar spoof and the classic building block for credential-phishing. It does not corrupt memory, so it is a spoofing / origin-confusion issue rather than RCE, but address-bar integrity is a core security-boundary property of the browser. The fix is a one-line MESSAGE_CHECK, confirming the pre-patch path performed no origin validation at all.
Same-document navigations (history.pushState / replaceState and fragment changes) are the one navigation class where the URL changes without a network load, so the UI process must trust the web process’s word for the new URL. The bug is that it trusted it completely: any URL, cross-origin included, was accepted for the frame. The fix reintroduces the invariant that a same-document navigation can only move within the same protocol/host/port as the frame’s current URL.
Root cause
WebPageProxy::didSameDocumentNavigationForFrameViaJS runs in the UI process and handles the IPC a web content process sends when script performs a same-document navigation — history.pushState, history.replaceState, or a fragment navigation. Its job is to update the UI process’s record of the frame’s current URL, which in turn drives the visible address bar and the navigation/back-forward state.
By definition a same-document navigation does not load a new document: it only changes the URL (and history entry) of the existing document. The security invariant is therefore that the new URL must be same-origin with — more precisely, same protocol, host and port as — the document currently loaded in the frame. The HTML history API enforces exactly this on the web-content side: pushState throws a SecurityError if the new URL is not same-origin. But the UI process cannot trust the web process to have honoured that check, because in the threat model the web process may be compromised and can forge the IPC directly.
Before the patch the handler validated only that the URL was structurally acceptable (MESSAGE_CHECK_URL, which guards against malformed/unparseable URLs) and then accepted whatever URL the message carried as the frame’s new committed URL. A malicious web process could thus send didSameDocumentNavigationForFrameViaJS with, say, https://www.apple.com/ while actually displaying attacker-controlled content, and the UI process would update the frame URL — and hence the address bar — to the spoofed origin.
The fix adds a single MESSAGE_CHECK asserting url.protocolIsFile() || frame->url().isEmpty() || protocolHostAndPortAreEqual(url, frame->url()). protocolHostAndPortAreEqual re-imposes the same-origin (scheme+host+port) constraint against the frame’s current URL, so a same-document navigation can no longer jump the frame to a different origin. The frame->url().isEmpty() clause permits the initial/empty case, and protocolIsFile() accommodates local file URLs where host/port comparison is not meaningful. A MESSAGE_CHECK failure in WebKit terminates the offending web process, so a forged cross-origin message is now fatal to the sender rather than silently trusted.
Key code
WebPageProxy.cpp: same-origin MESSAGE_CHECK added to didSameDocumentNavigationForFrameViaJS
Ref process = WebProcessProxy::fromConnection(connection);
MESSAGE_CHECK_URL(process, url);
MESSAGE_CHECK(process, url.protocolIsFile() || frame->url().isEmpty() || protocolHostAndPortAreEqual(url, frame->url()));
WEBPAGEPROXY_RELEASE_LOG(Loading, "didSameDocumentNavigationForFrameViaJS: frameID=%" PRIu64 ", isMainFrame=%d, type=%u", frameID.toUInt64(), frame->isMainFrame(), std::to_underlying(navigationType));
Patch walkthrough
Source/WebKit/UIProcess/WebPageProxy.cpp— In WebPageProxy::didSameDocumentNavigationForFrameViaJS, immediately after the existing MESSAGE_CHECK_URL(process, url) structural check, a new MESSAGE_CHECK enforces url.protocolIsFile() || frame->url().isEmpty() || protocolHostAndPortAreEqual(url, frame->url()). This validates the sender’s claimed same-document URL against the frame’s current origin before it is accepted, and kills the web process if the claim is cross-origin. The rest of the handler (release logging, state update) is unchanged.
Background
Same-document navigation — A same-document navigation changes a frame’s URL and history entry without fetching or loading a new document — history.pushState, history.replaceState, and same-document fragment (hash) navigations. Because no network commit occurs, the URL update is driven entirely by script running in the web content process, and the UI process learns of it only through the didSameDocumentNavigationForFrame IPC family. The web-side history API restricts these to same-origin URLs, but that restriction lives in the (untrusted) web process.
MESSAGE_CHECK IPC hardening — MESSAGE_CHECK (and its variants MESSAGE_CHECK_URL, MESSAGE_CHECK_COMPLETION) is WebKit’s macro for validating attacker-controllable IPC arguments in the UI/Network process. If the predicate is false the receiving process treats the message as malicious and terminates the sending web process via its Connection, rather than proceeding on bad data. It is the primary mechanism by which WebKit re-checks, on the trusted side of the sandbox boundary, invariants that the web process is nominally supposed to uphold — the correct place to enforce origin constraints under a compromised-renderer threat model.
protocolHostAndPortAreEqual and the origin tuple — protocolHostAndPortAreEqual compares two URLs on their (scheme, host, port) tuple — the tuple that defines a web origin for same-origin purposes. Using it here means a same-document navigation may only move the frame within its current origin. The extra protocolIsFile() disjunct handles file: URLs (where host/port equality is not the right test) and frame->url().isEmpty() handles the not-yet-navigated frame, so legitimate cases are not blocked while cross-origin jumps are.
Address-bar integrity as a security boundary — The address bar is the user’s only trustworthy signal of which origin they are interacting with; anti-phishing, credential entry decisions, and permission grants all key off it. The UI process derives the displayed URL from its own record of each frame’s committed URL, which is why letting the web process dictate an arbitrary same-document URL directly compromises that signal. Address-bar spoofs are treated as genuine security vulnerabilities (with CVEs) precisely because they defeat the user’s ability to authenticate the site.
UI-process vs web-process trust split — In WebKit’s multiprocess model the web content process is the sandboxed, potentially-compromised principal, while the UI process holds authority over chrome, navigation state and cross-origin policy. Any security decision the web process is documented to make (like the history API’s same-origin check) must be independently re-validated by the UI process before it acts on the result, because a memory-corrupted web process can emit arbitrary IPC. This patch closes one spot where that re-validation was missing.
Vulnerability window
- Original design — didSameDocumentNavigationForFrameViaJS relied on the web process’s history API to have already enforced the same-origin restriction, and validated only URL well-formedness (MESSAGE_CHECK_URL) on receipt.
- Threat-model gap — Under the compromised-renderer threat model the web process can forge the IPC directly, so the missing UI-process re-check let a malicious renderer set the frame URL — and the address bar — to any origin.
- Report — Reported as bug 310073 / rdar://172567659: didSameDocumentNavigationForFrame accepts arbitrary URL, enabling address bar spoofing.
- Branch fix — Originally landed on a Safari release branch as 305413.512@rapid/safari-7624.2.5.110-branch (d1551df53d97), rdar://176062692, indicating it was shipped to users via a rapid/security update path first.
- Mainline — Merged to trunk as commit 313796@main by Chris Dumez, reviewed by Ryosuke Niwa, adding the one-line same-origin MESSAGE_CHECK.
Triggering
No PoC or test is included in the patch. Conceptual trigger: from a compromised web content process (or an IPC fuzzer standing in for one), send the UI process a didSameDocumentNavigationForFrameViaJS message for the main frame with url set to a cross-origin https URL (e.g. https://bank.example/) while the frame actually holds attacker-controlled content. On an unpatched build the UI process accepts it and the address bar shows the spoofed origin; on a patched build the MESSAGE_CHECK fails and the web process is terminated. A pure-script analogue is not available because the history API’s own same-origin check blocks the cross-origin pushState before the IPC is sent — reaching the bug requires forging the IPC, i.e. a renderer already under attacker control.
Exploitation
- Prerequisite — Attacker needs to emit a crafted didSameDocumentNavigationForFrameViaJS IPC, which requires either a compromised web content process (a chained renderer bug) or an equivalent IPC-injection position — the normal history API path enforces same-origin and cannot reach the flaw.
- Spoof — Send the message with a cross-origin (typically https) URL for the main frame; the UI process updates its committed-URL record and the address bar renders the attacker-chosen origin while the displayed document remains attacker content.
- Phishing payoff — With the address bar showing a trusted origin, the attacker presents a spoofed login or consent UI; the user, trusting the bar, enters credentials or grants permissions to what appears to be the legitimate site.
- No memory-safety impact — This bug yields origin/URL confusion only; it does not itself corrupt memory or escape the sandbox. Its exploitation value is as the deception layer atop a separate renderer-compromise primitive, or as a standalone phishing amplifier where an IPC-injection foothold exists.
Detection & hunting
For defenders and SOC / detection engineers:
- Web-process termination from a failed MESSAGE_CHECK in didSameDocumentNavigationForFrameViaJS — On patched builds, a renderer attempting the spoof is killed; monitor crash/termination telemetry for WebPageProxy MESSAGE_CHECK failures on this handler as a signal of a compromised or misbehaving renderer.
- Mismatch between committed frame origin and rendered content origin — Where instrumentation is possible, correlate the UI-process frame URL against the actual document origin reported by the web process; a same-document navigation that crosses origins is anomalous by construction.
- Vulnerable build fingerprint — Detect WebKit/Safari builds predating 313796@main (and lacking the branch backport 305413.512) for exposure assessment; the flaw is not probeable from ordinary script since the history API blocks the cross-origin call client-side.
Audit directions
- Audit the full didSameDocumentNavigationForFrame IPC family — Review every UI-process entry point that accepts a frame URL from the web process (didSameDocumentNavigationForFrameViaJS and its non-JS siblings, didChangeMainDocument, didCommitLoadForFrame paths) for the presence of an origin re-check against the frame’s current URL, not just MESSAGE_CHECK_URL well-formedness.
- Grep for URL-bearing IPC without protocolHostAndPortAreEqual — Search WebPageProxy and related UI-process handlers for parameters of type URL that feed navigation/address-bar state and confirm each performs an origin or same-document validation; the pattern MESSAGE_CHECK_URL-without-follow-up is the tell.
- Consistency of the same-origin predicate — Verify the protocolIsFile()/isEmpty()/protocolHostAndPortAreEqual triad is applied uniformly wherever same-document URL changes are accepted, so file: and initial-empty edge cases are handled the same way and do not become new bypasses.
- Site-isolation interaction — With Site Isolation, same-document navigations may involve RemoteFrame/LocalFrame transitions; confirm the frame->url() used in the check reflects the correct (local) frame’s committed origin and cannot be desynchronised across processes to defeat the comparison.