[3] `WebPageProxy::didFailLoadForFrame` accepts an unvalidated failing URL
A compromised renderer names a file it can't read, and Safari hands it back.
Rated High: there is no memory corruption anywhere in this chain, but the payoff is a file:// read grant extended to a WebContent process that never held one. Escalation is gated on a renderer already being compromised plus an embedder that reflects the failing URL back through the alternate-HTML API — which is exactly what MobileSafari does.
WebKit runs web content in a sandboxed process that the UI process treats as untrusted; every URL crossing that boundary upward is supposed to be checked against the set of URLs the sending process was actually granted. WebPageProxy is the UI-process-side representative of a page — it receives load-progress and load-failure messages from the WebContent process and translates them into calls on the embedding app's navigation client. The enforcement idiom is MESSAGE_CHECK_URL, which validates a URL against the sending process's allowed set and terminates the connection on mismatch; any WebContent-supplied URL that can influence privileged behavior is expected to pass through it before use.
The angle: a compromised WebContent process can report a load failure naming any file:// URL and, once the embedder reflects it back through the alternate-HTML API, receive read access to the enclosing directory in both the UI and Network processes.
Patch Details
WebPageProxy::didFailLoadForFrame already validated the frame and frameInfo and logged the error's domain and code. The patch adds MESSAGE_CHECK_URL(process, error.failingURL()) before the WebCore::ResourceError is handed onward to m_navigationClient->didFailNavigationWithError / the loader client, bringing the handler in line with the validation its sibling entry point didFailProvisionalLoadForFrameShared already performed on the same field.
Untrusted input validated on one entry point but not on its sibling, then laundered back through an API that treats the same field as embedder-authorized.
Background
The process model. WebKit splits work across four process roles: WebContent (runs untrusted page content, sandboxed), GPU, Networking, and UI. The UI process hosts the embedding application and is the sandbox-policy authority; it is the process that hands out sandbox extensions granting filesystem reach.
WebPageProxy and IPC endpoints. Each web page has a WebPageProxy in the UI process. Its message handlers are directly reachable from the WebContent process, which makes them the primary chokepoint for validating renderer-supplied data.
MESSAGE_CHECK and MESSAGE_CHECK_URL. These macros express the trust boundary in code: MESSAGE_CHECK kills the connection when an invariant on IPC-supplied data fails, and MESSAGE_CHECK_URL specializes it to URL validation against the sending process's granted set. They exist so that a validation failure is a hard, non-recoverable outcome rather than a logged anomaly — a compromised renderer should lose the connection, not get a fallback path.
ResourceError over IPC. WebCore::ResourceError is fully serializable and is reconstructed verbatim from the IPC payload through ResourceError::fromIPCData / CoreIPCError. Its failingURL() is a URL field whose contents are whatever the sender wrote.
Alternate HTML. -[WKWebView _loadAlternateHTMLString:baseURL:forUnreachableURL:] is the API embedders use to display error pages in place of a failed load. It reaches WebPageProxy::loadAlternateHTML, and it treats its base/unreachable URL as embedder-authorized.
Analysis
This is a missing IPC input-validation bug — a trust-boundary and capability-confusion logic defect, not a memory-safety one. The invariant that went unenforced is WebKit's standing rule for the UI process: any URL originating in a WebContent process that can influence privileged behavior must pass MESSAGE_CHECK_URL against that process's granted URL set before it is used or forwarded. Coverage across the two load-failure paths was asymmetric — the provisional path enforced it, the committed path did not.
WebContent (sandboxed) UI process Network process
---------------------- ---------- ---------------
DidFailLoadForFrame
error.failingURL =
"file:///Users/..." ---> didFailLoadForFrame
[no MESSAGE_CHECK_URL]
|
v
navigationClient->
didFailNavigationWithError
| (NSError, embedder-trusted)
v
embedder re-enters:
_loadAlternateHTMLString:
forUnreachableURL:
|
v
loadAlternateHTML --> grants dir read ---> same grant
The laundering is the whole exploit. The failingURL enters the UI process as untrusted WebContent-supplied data and exits it as a WebKit-vended NSError / WKNavigationDelegate argument — a value embedders reasonably treat as authoritative, because everything else the UI process vends through that interface is. The commit message identifies the concrete loop on iOS: MobileSafari takes the failingURL from the delegate callback and passes it straight back in as forUnreachableURL: to -[WKWebView _loadAlternateHTMLString:baseURL:forUnreachableURL:], reaching WebPageProxy::loadAlternateHTML.
Because that API treats its base and unreachable URLs as embedder-authorized, a file:// value causes read access to the enclosing directory to be extended to the sending WebContent process — in the UI process and in the Network process both. The net effect is that a WebContent process that never held a file:// grant can name one and receive it.
Exploitability requires an already-compromised renderer, since a well-behaved WebContent process has no way to emit a fabricated ResourceError; that is the standard second-stage position for a sandbox escape, and this is a clean escalation primitive from it. No timing, grooming, or memory-layout control is involved — the sequence is deterministic.
This vulnerability weakens the WebContent-to-UI privilege boundary at one of its enforcement chokepoints: the process-granted URL set stops being the authority on what a renderer can name, because the renderer can name a path and have the UI process ratify it on the way back out.
Audit directions
- Paired IPC entry points with asymmetric validation. Handlers that come in provisional/committed or plain/
...Sharedpairs are the classic place for one half to drift out of coverage. Enumerate theWebPageProxyload and navigation handlers and diff each pair's validation on shared fields. In code review, two adjacent handlers taking the same parameter type where only one calls aMESSAGE_CHECK*macro is the tell. - Untrusted values laundered through embedder-facing APIs. Any field that exits the UI process as an
NSErroror delegate argument and can re-enter through public or SPI is a round-trip laundering candidate, because the re-entry point applies embedder-level trust. Start fromResourceError::failingURL()and trace every consumer that forwards it outward. - APIs that treat a URL parameter as pre-authorized.
loadAlternateHTMLgrants on the strength of its caller. Audit the other UI-process entry points that extend sandbox reach based on a caller-supplied path, and confirm that every route into them carries a validated URL rather than a relayed one.