← All issues

[1] WebAuthn UIProcess origin spoofing via unvalidated IPC fields

Severity: High | Component: WebKit UIProcess WebAuthentication | 315ac30

Rated High because the diff removes a missing-validation gap at the WebContent-to-UIProcess boundary that let a compromised renderer bind a WebAuthn ceremony to an arbitrary relying-party origin; escalation to cross-site credential impersonation requires only a prior renderer compromise, which is the assumed threat model, though the bug yields no memory-corruption primitive.

A compromised WebContent process could spoof the securityOrigin in FrameInfoData or the parentOrigin parameter when sending WebAuthn MakeCredential/GetAssertion IPC messages to the UI process. This would let an attacker page impersonate a different origin (e.g. a bank) for credential creation or assertion. This patch adds MESSAGE_CHECKs to prevent that.

Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.cpp

-void WebAuthenticatorCoordinatorProxy::getAssertion(FrameIdentifier frameId, FrameInfoData&& frameInfo, ... std::optional<WebCore::SecurityOriginData> parentOrigin, RequestCompletionHandler&& handler)
+void WebAuthenticatorCoordinatorProxy::getAssertion(IPC::Connection& connection, FrameIdentifier frameId, FrameInfoData&& frameInfo, ... std::optional<WebCore::SecurityOriginData> parentOrigin, RequestCompletionHandler&& handler)
{
RefPtr webPageProxy = m_webPageProxy.get();
...
+ RefPtr frame = WebFrameProxy::webFrame(frameId);
+ if (!frame) {
+ RELEASE_LOG_ERROR(WebAuthn, "Frame not found for WebAuthn GetAssertion request");
+ return handler({ }, static_cast<AuthenticatorAttachment>(0), ExceptionData { ExceptionCode::InvalidStateError });
+ }
+ if (frame->url().protocolIsInHTTPFamily()) {
+ auto expectedOrigin = SecurityOriginData::fromURLWithoutStrictOpaqueness(frame->url());
+ MESSAGE_CHECK_COMPLETION_BASE(frameInfo.securityOrigin == expectedOrigin, connection,
+ handler({ }, static_cast<AuthenticatorAttachment>(0), ExceptionData { ExceptionCode::InvalidStateError }));
+ }
+ if (parentOrigin) {
+ bool foundMatchingAncestor = false;
+ bool hasHTTPAncestor = false;
+ for (RefPtr ancestor = frame->parentFrame(); ancestor; ancestor = ancestor->parentFrame()) {
+ if (!ancestor->url().protocolIsInHTTPFamily())
+ continue;
+ hasHTTPAncestor = true;
+ auto ancestorOrigin = SecurityOriginData::fromURLWithoutStrictOpaqueness(ancestor->url());
+ if (*parentOrigin == ancestorOrigin) { foundMatchingAncestor = true; break; }
+ }
+ if (hasHTTPAncestor)
+ MESSAGE_CHECK_COMPLETION_BASE(foundMatchingAncestor, connection, handler(...));
+ }
handleRequest({ ..., WTF::move(frameInfo), ..., parentOrigin }, WTF::move(handler));
}

LayoutTests/http/tests/ipc/web-authenticator-get-assertion-spoofed-origin-crash.html

+const spoofedOrigin = { data: { variantType: 'WebCore::SecurityOriginData::Tuple', variant: { protocol: 'https', host: 'evil.com', port: {} } } };
+CoreIPC.UI.WebAuthenticatorCoordinatorProxy.GetAssertion(IPC.webPageProxyID, { frameInfo: { ... securityOrigin: spoofedOrigin, topOrigin: spoofedOrigin, ... }, options: { ... }, mediation: 0, parentOrigin: {} });

Both makeCredential and getAssertion now take an IPC::Connection& as their first parameter so the receivers can issue connection-terminating checks. Each handler looks up the real frame via WebFrameProxy::webFrame(frameId), bails with InvalidStateError if none is found, and — when the frame URL is HTTP(S) — recomputes the expected origin with SecurityOriginData::fromURLWithoutStrictOpaqueness(frame->url()) and asserts frameInfo.securityOrigin == expectedOrigin via MESSAGE_CHECK_COMPLETION_BASE. getAssertion additionally walks frame->parentFrame() ancestors and, if any HTTP(S) ancestor exists, requires parentOrigin to equal one of those ancestor origins. Two IPC-testing layout tests send spoofed-origin requests and expect the WebContent process to be terminated.

Trusting an IPC-supplied security origin from the untrusted WebContent process without re-validating it against UI-process-owned frame state.

WebKit splits work between a sandboxed WebContent (renderer) process and a privileged UI process; security-sensitive operations cross this boundary as IPC messages, and the UI process must treat every field as attacker-controlled. The MESSAGE_CHECK/MESSAGE_CHECK_COMPLETION_BASE macros terminate the offending IPC connection — killing the WebContent process — when their predicate fails, which is why the regression tests are named *-crash. FrameInfoData.securityOrigin is the renderer's claim about a frame's origin; WebFrameProxy::webFrame(frameId) returns the UI process's own record of that frame, whose url() is authoritative. WebAuthn MakeCredential/GetAssertion perform credential ceremonies scoped to a relying-party origin, and for cross-origin (iframe) assertions a parentOrigin describes the embedder, which must match an actual ancestor frame's origin.

This is an origin-spoofing / authority-confusion bug, not memory corruption. Before the fix, the UI-process WebAuthn receivers consumed frameInfo.securityOrigin (and the parentOrigin argument) directly from the IPC message and propagated them into handleRequest/buildClientDataJson without checking them against the authoritative frame state. In WebKit's IPC trust model the renderer is untrusted: any field it serializes must be re-validated against UI-process-owned ground truth, and here that step was simply absent.

🔒

Examines how a renderer-supplied identity field crosses into a privileged process and what an attacker can claim before the boundary is enforced.

Subscribe to read more

🔒

Multiple reusable audit patterns identified for IPC origin-validation gaps, with concrete UIProcess starting points for variant discovery.

Subscribe to read more