← All reports

[3] `WebPageProxy::didFailLoadForFrame` accepts an unvalidated failing URL

HighWebKit UI processAuthBypass

A compromised renderer names a file it can't read, and Safari hands it back.

248f89a

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.

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.

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.

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.