← WebKit — 2026-W41

26556dce7408f596727c3ed63f6eb0bd15c49bbb  Digital Credentials: derive request origins from trusted UI-process state

severity medium class Spoofing confidence 0.6 signal score 6.17 Source/WebKit/UIProcess patch-now AI review CONFIRMED
Primitive: UIProcess trusted renderer-supplied topOrigin/documentOrigin for Digital Credentials request
Triage note: showDigitalCredentialsChooser now MESSAGE_CHECKs that requestingFrame->process() matches the IPC connection and overrides sanitizedRequestData.topOrigin/documentOrigin with mainFrame->documentSecurityOriginData()/requestingFrame->documentSecurityOriginData() instead of passing the renderer-supplied requestData; prevents a compromised/colluding renderer from spoofing the origins shown in the credentials chooser.
Structural signals: smartptr_member_added (+4.87), surgical (+1.3)

The bug at a glance

A UIProcess spoofing bug: WebPageProxy::showDigitalCredentialsChooser trusted the WebContent-process-supplied requestData.topOrigin/documentOrigin when presenting the Digital Credentials consent UI, and only checked that the renderer-claimed topOrigin was same-origin-domain with the main frame URL. A compromised or colluding web process could therefore control the origins displayed in a security-critical identity-release prompt, tricking the user into disclosing a digital credential (e.g. a mobile driver’s license / identity document) to the attacker’s site while the chooser shows a trusted origin. This is medium-severity spoofing / origin confusion in the UIProcess, not RCE; impact is user deception leading to credential disclosure. The fix is a clear UIProcess trust-boundary correction.

The security UI that asks the user to release a government-identity credential was being labelled with strings the untrusted renderer supplied, so the one party the prompt must authenticate (the requester) was attacker-controlled.

Root cause

The invariant: the UIProcess, which owns the trusted consent UI, must derive the origins it displays from its own authoritative frame state (WebFrameProxy), never from the IPC payload a WebContent process sends. Pre-patch, showDigitalCredentialsChooser(connection, …, requestData, completionHandler) performed exactly one gate — MESSAGE_CHECK_COMPLETION_BASE that requestData.topOrigin.securityOrigin()->isSameOriginDomain(SecurityOrigin::create(mainFrame()->url())) — and then passed the renderer-supplied requestData straight to pageClient()->showDigitalCredentialsChooser(requestData, …). Two problems follow: (1) requestData.documentOrigin was never validated at all; (2) the topOrigin was only compared, never replaced, and the frame the request claimed to come from was never checked against the sending process.

The pre-patch sequence a compromised renderer could run: 1. craft a ShowDigitalCredentialsChooser IPC with requestData.topOrigin/documentOrigin set to a trusted site (e.g. a real identity verifier); 2. ensure the main frame URL is same-origin-domain with the forged topOrigin (or navigate so the weak isSameOriginDomain check passes); 3. UIProcess forwards the forged origins verbatim to the platform chooser; 4. the user sees a legitimate-looking requester and approves release of their credential, which flows back to the attacker-controlled context.

The fix resolves requestingFrame = WebFrameProxy::webFrame(frameID) and mainFrame = this->mainFrame(), bails with AbortError if either is missing or requestingFrame->page() != this, and MESSAGE_CHECKs &requestingFrame->process() == WebProcessProxy::fromConnection(connection).ptr() so a process cannot speak for a frame it does not host. It then builds sanitizedRequestData whose topOrigin = mainFrame->documentSecurityOriginData() and documentOrigin = requestingFrame->documentSecurityOriginData() — UIProcess-authoritative values — and passes sanitizedRequestData, not requestData, to the chooser. These are WebFrameProxy/WebProcessProxy objects on the UIProcess side; the identifiers crossing IPC (FrameIdentifier, connection) are the only renderer inputs, and both are now validated. No lifetime bug is involved; this is a trust/origin-provenance fix.

Key code

Validate sender owns the frame, then override origins with UIProcess-authoritative values

RefPtr requestingFrame = WebFrameProxy::webFrame(frameID);
RefPtr mainFrame = this->mainFrame();
if (!requestingFrame || !mainFrame || requestingFrame->page() != this) { /* AbortError */ return; }
MESSAGE_CHECK_COMPLETION_BASE(
    &requestingFrame->process() == WebProcessProxy::fromConnection(connection).ptr(), connection, ...);
auto sanitizedRequestData = requestData;
sanitizedRequestData.topOrigin = WTF::move(mainFrame->documentSecurityOriginData());
sanitizedRequestData.documentOrigin = WTF::move(requestingFrame->documentSecurityOriginData());
protect(pageClient())->showDigitalCredentialsChooser(sanitizedRequestData, WTF::move(completionHandler));

Fix approach

The guard moves from “compare the renderer’s claimed topOrigin to the main frame URL” to “never trust the renderer’s origins at all.” The new holder objects are the UIProcess-side WebFrameProxy (requestingFrame, mainFrame); the patch first proves the sending connection actually hosts requestingFrame (frame-id/process binding) and that the frame belongs to this page, then recomputes both displayed origins from documentSecurityOriginData() of those proxies. The displayed topOrigin becomes the main frame’s real origin and documentOrigin the requesting frame’s real origin, so the only renderer-controlled field left feeding the UI is eliminated. A behavioural change: the old same-origin-domain-with-top refusal is replaced by a generic “not allowed” error plus an explicit missing-frame AbortError path.

Patch walkthrough

  • Source/WebKit/UIProcess/WebPageProxy.cpp — In showDigitalCredentialsChooser: resolves and validates requestingFrame/mainFrame, replaces the renderer-topOrigin same-origin MESSAGE_CHECK with a process-owns-frame MESSAGE_CHECK, and constructs sanitizedRequestData with UIProcess-derived topOrigin/documentOrigin before presenting the chooser.

Background

Digital Credentials API — Web API (navigator.credentials / digital credential request) that asks the platform to release an identity document such as a mobile driver’s license; release is gated by a trusted system chooser UI that names the requesting site.

UIProcess vs WebContent trust boundary — The UIProcess is trusted and owns native UI and authoritative frame/origin state; WebContent processes are untrusted and can be fully compromised, so any security string shown to the user must originate in the UIProcess.

WebFrameProxy / WebProcessProxy — UIProcess-side authoritative representations of a web frame and its hosting process; documentSecurityOriginData() yields the origin the UIProcess actually recorded for that frame, independent of renderer claims.

MESSAGE_CHECK_COMPLETION_BASE — A WebKit IPC guard macro that validates an invariant on an incoming message and, on failure, runs a completion handler (here returning a SecurityError) and treats the message as hostile.

documentSecurityOriginData() — Returns the SecurityOriginData the UIProcess holds for a frame’s current document; used here as the trusted source for the origins shown in the chooser.

Vulnerability window

  1. Setup — Attacker controls (or compromises) a WebContent process able to issue the ShowDigitalCredentialsChooser IPC.
  2. Forge — It sends requestData with topOrigin/documentOrigin set to a trusted identity-requester site.
  3. Weak check — Pre-patch UIProcess only verifies the claimed topOrigin is same-origin-domain with the main frame URL; documentOrigin unchecked.
  4. Present — UIProcess forwards the forged origins to the native credentials chooser.
  5. Disclose — User, seeing a trusted requester, approves and the credential is released to the attacker’s context.

Proof of concept

No regression test ships in the diff. Conceptual trigger requires a compromised/instrumented WebContent process that emits WebPageProxy::ShowDigitalCredentialsChooser with attacker-chosen requestData.topOrigin/documentOrigin while the page is navigated so the pre-patch isSameOriginDomain-with-top check passes; the web path and the exact credential-release consequence are (inferred) from the UIProcess code.

Exploitation

  1. Reach — Requires a compromised or colluding WebContent process (standard site-isolation attacker model for UIProcess-side checks).
  2. Trigger — Emit the chooser IPC with forged origins; satisfy the weak same-origin-domain-with-top gate via navigation/same-registrable-domain.
  3. Primitive — Control of the requester/top origin strings shown in a trusted identity-release prompt (UI spoofing / origin confusion).
  4. Escalation — User-facing phishing of a high-value identity credential; escalation beyond deception (e.g. silent release) is not supported — a user approval is still required.

Detection & hunting

For defenders and detection engineers:

  • Chooser origins that disagree with UIProcess frame state — Compare requestData.topOrigin/documentOrigin against WebFrameProxy::documentSecurityOriginData() at the chooser call and log mismatches.
  • ShowDigitalCredentialsChooser from a process not hosting the claimed frame — Flag IPCs where &requestingFrame->process() != WebProcessProxy::fromConnection(connection).
  • Frame-id with no live WebFrameProxy — Monitor the new AbortError (“no active frame”) path for abuse/fuzzing.

Remaining risk in the patch

  • Main-frame origin for the top label — topOrigin is set from mainFrame->documentSecurityOriginData(); for a cross-origin requesting subframe the chooser now shows the top document’s origin as topOrigin, which is correct but means the trust decision still hinges on the UIProcess frame tree being accurate for the active main frame.
  • Residual reliance on frame state freshness — documentSecurityOriginData() must reflect the frame’s current document at prompt time; a race between navigation and chooser presentation is not addressed by the diff and is unverified here.

Audit directions

  • Other UIProcess consent prompts — Audit WebAuthn, getUserMedia, geolocation, file-system-access and payment choosers for origins/strings taken from renderer IPC rather than WebFrameProxy.
  • Renderer-supplied SecurityOriginData in WebPageProxy.messages.in — Enumerate messages carrying topOrigin/documentOrigin and verify the UIProcess re-derives rather than trusts them.
  • FrameIdentifier-to-process binding — Look for handlers that resolve WebFrameProxy::webFrame(frameID) without checking the frame’s process equals the sending connection.

Before / after

Loading diff…