[2] Digital Credentials chooser trusted renderer-supplied origins
A compromised renderer could put any site's name on the system credential chooser.
Medium, and only once the renderer is already owned. Nothing here corrupts memory, but a compromised WebContent process could put a victim site's origin on trusted system consent UI. Whether that becomes a real credential misbinding depends on how the platform chooser uses the origins it receives.
Consent UI that WebKit's multi-process model brokers depends on the privileged UI process deciding who is asking, without taking the sandboxed renderer's word for it. Requests from the Digital Credentials API arrive at WebPageProxy, the UI-process object that receives a page's IPC messages. That API asks a platform wallet to present an identity document such as a mobile driver's license, and each request carries a frame identifier plus the top-level origin and the requesting document's origin. The identity shown in the system credential chooser is supposed to come from UI-process state bound to the connection that sent the message.
The angle: an attacker who already controls a WebContent process could open the system credential chooser under a forged requesting origin. A user could then be led to release an identity credential while the chooser shows a trusted relying party.
Digital Credentials: derive request origins from trusted UI-process state. WebPageProxy::showDigitalCredentialsChooser presented the credential chooser using the WebContent-supplied origins in requestData, which a compromised WebContent process controls. The fix resolves the requesting frame from the IPC frame identifier, rejects the request if it names no frame in this page, and message-checks that the IPC came from that frame's process. It then derives the top-level and requesting origins from the UI process's trusted frame state and sanitizes requestData with them before presenting the chooser. Originally landed as 316606.155@safari-7625-branch.
Source/WebKit/UIProcess/WebPageProxy.cpp
LayoutTests/ipc/digital-credentials-chooser-frame-check.html
Patch Details
Inside the HAVE(DIGITAL_CREDENTIALS_UI) branch of WebPageProxy::showDigitalCredentialsChooser, the patch makes three changes. First, it resolves the requesting frame with WebFrameProxy::webFrame(frameID) and takes this->mainFrame(). If either is null, or if requestingFrame->page() != this, it rejects the request with an AbortError. Second, it replaces the old MESSAGE_CHECK_COMPLETION_BASE condition. The old check compared the renderer-supplied requestData.topOrigin against SecurityOrigin::create(mainFrame()->url()) with isSameOriginDomain. The new one requires &requestingFrame->process() == WebProcessProxy::fromConnection(connection).ptr(), and its failure message becomes the generic "Digital credentials request is not allowed." Third, after user activation is consumed, it builds sanitizedRequestData, a copy whose topOrigin and documentOrigin are overwritten with documentSecurityOriginData() from the main and requesting WebFrameProxy. That copy goes to PageClient::showDigitalCredentialsChooser in place of the raw data. A new IPC layout test sends a forged message with a null frameID, and a glib TestExpectations entry skips it.
Confused deputy at a privilege boundary: a broker keys a consent decision on a principal the unprivileged client self-reports, not on broker-side state bound to the authenticated caller.
Background
Where this lives.
WebKit runs web content in sandboxed WebContent processes. GPU and Networking processes handle graphics and network I/O, and the UI process, which is the app embedding WebKit, holds trusted state such as the frame tree, committed origins and user-activation timestamps. WebContent talks to the UI process over IPC, and classes like WebPageProxy receive those messages.
IPC validation macros.
MESSAGE_CHECK and its variant MESSAGE_CHECK_COMPLETION_BASE are WebKit's standard defense against a compromised renderer. When the condition fails, the message is treated as coming from a misbehaving process: it is marked invalid, which typically terminates that WebContent process, and the supplied completion runs.
WebFrameProxy and site isolation.
WebFrameProxy is the UI process's representation of a frame. WebFrameProxy::webFrame(frameID) looks a frame up in a UI-process map, page() returns the owning WebPageProxy, and process() returns the WebProcessProxy that hosts the frame. Under site isolation, frames of one page can live in different WebContent processes, so knowing the page does not tell you which process sent a message.
Committed origin vs. URL-derived origin.
documentSecurityOriginData() is the origin of the document committed in a frame. SecurityOrigin::create(url) computes an origin from a URL string. The two can differ for documents whose origin is inherited or opaque, such as about:blank or sandboxed frames.
Digital Credentials.
navigator.credentials.get with a digital member lets a site ask a platform wallet for an identity document, for example an ISO mdoc mobile driver's license. The request data (DigitalCredentialsMobileDocumentRequestData) carries topOrigin and documentOrigin. The browser shows a system chooser where the user consents to sharing the credential with that requester.
Analysis
This is a logic error, specifically missing IPC validation: the UI process treated the serialized request data as authoritative about who was asking.
WebContent (compromised) UI process (WebPageProxy)
┌───────────────────────┐ ┌──────────────────────────────────────┐
│ ShowDigitalCredentials│ IPC │ Before: topOrigin ~ create(mainURL)? │
│ Chooser(frameID, │ ───────► │ documentOrigin: unchecked │
│ topOrigin, │ │ frameID: not bound to sender │
│ documentOrigin) │ │ requestData ──► system chooser │
└───────────────────────┘ │ After: frame in page? sender hosts │
│ it? origins overwritten from │
│ WebFrameProxy ──► system chooser │
└──────────────────────────────────────┘
The "Before" box in the diagram has three gaps. (a) documentOrigin, the origin of the frame actually making the request, was never validated and was forwarded unchanged to the chooser. (b) Within the HAVE(DIGITAL_CREDENTIALS_UI) branch, the IPC frameID was never resolved or bound to the sending connection. This covers a null frameID, a frame belonging to another page, or a frame hosted by a different WebContent process. The last case matters under site isolation, where several processes talk to one WebPageProxy. Whether any earlier part of showDigitalCredentialsChooser constrained frameID is an open question; if nothing did, all three cases reached the chooser. (c) Even the topOrigin check built its expected value from the main frame's URL and not its committed document origin, so the two could disagree for URL-less or inherited-origin documents.
The regression test runs the compromised-renderer threat model directly through the IPC testing API:
- Get
IPC.connectionForProcessTarget("UI"). - Serialize
WebPageProxy_ShowDigitalCredentialsChooserarguments withframeID: {}(null) and aDigitalCredentialsMobileDocumentRequestDatawhose origins and emptyrequestsare chosen by the test. - Send it and assert the rejection message "Digital credentials request has no active frame."
The test checks post-fix rejection only. The replaced branch did no frame resolution, so a null frameID would have reached the chooser unless an earlier part of showDigitalCredentialsChooser rejected it.
Ordinary web content cannot reach this. The renderer-side coordinator fills both origins honestly, and forging the message needs either IPCTestingAPIEnabled or a compromised WebContent process. With such a compromise, the pre-fix handler would accept forged origins and unbound frame identifiers and forward them to the platform chooser. That is a second-stage abuse of a brokered capability, not memory corruption. User consent and the consumption of lastActivationTimestamp, which still gates each request, bound how far it goes. The escalation question is how the chooser behind PageClient::showDigitalCredentialsChooser uses topOrigin and documentOrigin. If they are only display strings, the ceiling would be UI spoofing that relies on deceiving the user. If they feed origin-bound response construction, such as an origin-scoped session transcript or encryption context, a forged origin could let a compromised renderer obtain a credential presentation scoped to a victim relying party.
This vulnerability weakens the isolation boundary between a WebContent process and the UI process. The broken invariant is that identity claims shown in, or bound into, trusted system UI must come from UI-process state tied to the sending connection and never from data the renderer serialized. Before the fix, a compromised renderer could make the credential chooser display and act on a forged requesting origin, or on a topOrigin that passed the URL-derived check but did not match the committed document.
Insight: the pre-fix code did have a MESSAGE_CHECK, but it validated the wrong thing in the wrong way: one of two renderer-supplied origins, compared against a URL-derived origin, and no binding to the sending process. A validator that compares renderer data to broker state is weaker than one that throws the renderer data away and substitutes broker state, which is what the fix does. The requestingFrame->process() == WebProcessProxy::fromConnection(connection) comparison is the site-isolation-era form of this defense. Once several WebContent processes share one WebPageProxy, trusting the page is not enough, and every frameID carried over IPC has to be bound to its sender.
Audit directions
- Broker trusts a self-reported principal. A privileged broker that trusts an origin, URL, package or app identity reported by the unprivileged client is a confused deputy. Narrow: grep
Source/WebKit/UIProcessfor IPC handlers whose arguments includeSecurityOriginData,URL, or request structs with origin fields, and which use those values to drive permission prompts or system UI. Start with WebAuthn proxies, the share sheet, the contact picker, and media and notification permission handlers. The tell is any handler that reads an origin from the message instead of fromWebFrameProxy::documentSecurityOriginData()or a similar UI-process lookup. Wider: the same shape appears when a broker trusts frame URLs, top-frame URLs or user-activation flags reported by the renderer over its own records. Widest: this is the general rule never to trust the client's claim of who it is. Chromium encodes it as "useRenderFrameHost::GetLastCommittedOrigin, never a renderer-sent origin", Android Binder services asgetCallingUid()instead of a caller-supplied package name, and XPC services as audit tokens instead of self-reported PIDs. The tell anywhere is an authorization or consent decision that takes the principal from the request payload instead of from the authenticated channel. - Frame identifiers not bound to the sender. Check every UIProcess IPC handler that accepts a
FrameIdentifierand acts on a frame. Verify that it checks both that the frame belongs to this page and thatframe->process()is the process behind the connection. Narrow: grepWebPageProxy.cppand the other*Proxy.cppmessage receivers forWebFrameProxy::webFrame(followed by use of the frame with noprocess() == WebProcessProxy::fromConnection(connection)comparison; that missing comparison is the tell. Wider: any identifier-keyed lookup in a multi-tenant broker can show the same shape, including page, frame, worker or resource IDs looked up in a global map without an ownership check against the caller. This class is tied to WebKit's site-isolation model. The closest outside analog is Chromium's per-RenderProcessHostrouting-ID validation, though the exactMESSAGE_CHECKidiom does not carry over. - Expected origin derived from a URL. Audit security checks that build an expected origin with
SecurityOrigin::create(url)instead of using the committed document origin. Narrow: grepSource/WebKit/UIProcessforSecurityOrigin::create(applied tomainFrame()->url()or a frame'surl()insideMESSAGE_CHECKconditions or permission decisions. Each match is a place whereabout:blank,data:, blob or sandboxed documents could produce a mismatch. Wider: similar URL-vs-origin confusion appears in cookie partitioning, storage access and CSP reporting paths keyed on a URL. Widest: a document's security principal is a property of the committed document, not of its URL. Chromium's split betweenGetLastCommittedURLandGetLastCommittedOrigin, and the HTML spec's origin inheritance forabout:blankand sandboxed iframes, encode the same rule. The tell is a comparison where one side is computed from a URL and the other is a document or policy origin. - Sanitize-by-overwrite that misses fields or variants. Verify that sanitize-by-overwrite fixes cover every field the renderer supplies and every alternative of a
Variant. Narrow:DigitalCredentialsRequestDatais aVariantwith one current alternative (DigitalCredentialsMobileDocumentRequestData), andsanitizedRequestDataoverwrites onlytopOriginanddocumentOrigin. Check whether the remainingrequestspayload carries origin-scoped data and whether future alternatives will get equivalent sanitization. Wider: any broker that copies a client struct and patches selected fields fails open for fields added later. The tell is copy-then-overwrite with no exhaustive field or variant enumeration, such as aswitchOnwith a default branch.