315ac3073a Message check security origin during webauthn calls
Triage note: PoC spoofs evil.com origin over WebAuthenticatorCoordinatorProxy IPC; fix validates the origin, closing an origin-spoofing bypass.
Contents
The bug at a glance
WebAuthn origin binding is the entire security guarantee of the API: an assertion or credential must be scoped to the true origin of the requesting frame. A compromised WebContent process could set frameInfo.securityOrigin (and the getAssertion parentOrigin) to an arbitrary value such as https://bank.com over IPC, causing the UI process to perform credential ceremonies as if for that origin. That is a direct authentication-bypass / origin-spoofing primitive against phishing-resistant credentials, meriting high severity even though it is not memory corruption.
The angle is that WebAuthn’s anti-phishing property rests on origin data that, in this IPC, was supplied by the very process the model assumes may be compromised. The UI process trusted FrameInfoData.securityOrigin and parentOrigin verbatim; the fix re-derives the expected origin from the authoritative WebFrameProxy URL and message-checks the claimed origin against it.
Root cause
OBSERVED: WebAuthenticatorCoordinatorProxy::makeCredential and ::getAssertion run in the UI process and receive a FrameInfoData whose securityOrigin field, plus getAssertion’s optional parentOrigin parameter, were taken directly from the WebContent process and forwarded into handleRequest as the origin governing the WebAuthn ceremony. Nothing cross-checked those origins against the UI process’s own authoritative view of the frame.
INFERRED: WebAuthn relies on the relying-party origin to bind credentials; if a renderer can assert an arbitrary securityOrigin, an attacker page at evil.com can request a MakeCredential or GetAssertion ceremony that the platform and authenticator treat as originating from, say, a bank’s origin, undermining the phishing resistance that is the point of the API. For getAssertion, the parentOrigin parameter governs cross-origin (iframe) assertion policy, so spoofing it lets an attacker misrepresent the embedding ancestor chain.
OBSERVED: The fix changes both receiver signatures to take IPC::Connection& and adds validation. Both functions now look up the authoritative frame via WebFrameProxy::webFrame(frameId); if it is missing they fail the request with InvalidStateError. When the frame’s URL is in the HTTP(S) family, they compute expectedOrigin = SecurityOriginData::fromURLWithoutStrictOpaqueness(frame->url()) and MESSAGE_CHECK_COMPLETION_BASE(frameInfo.securityOrigin == expectedOrigin, connection, …), terminating the sender on mismatch. getAssertion additionally validates parentOrigin: it walks frame->parentFrame() ancestors, and if any HTTP(S) ancestor exists, requires that parentOrigin match at least one ancestor’s origin (foundMatchingAncestor), otherwise message-checks fail.
OBSERVED: The MESSAGE_CHECK is gated on protocolIsInHTTPFamily(), so non-HTTP schemes (e.g. app/custom schemes) retain prior behavior and are not subjected to the URL-derived origin equality check.
Key code
Origin validation added to getAssertion (parallel logic in makeCredential)
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({ }, static_cast<AuthenticatorAttachment>(0), ExceptionData { ExceptionCode::InvalidStateError }));
}
}
Patch walkthrough
Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.cpp— makeCredential and getAssertion gain an IPC::Connection& parameter and new validation: resolve the frame from frameId, bail with InvalidStateError if absent, and for HTTP(S) frames MESSAGE_CHECK_COMPLETION_BASE that frameInfo.securityOrigin equals SecurityOriginData::fromURLWithoutStrictOpaqueness(frame->url()). getAssertion also validates the optional parentOrigin against the HTTP(S) ancestor chain.Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.h— Updates the makeCredential/getAssertion receiver declarations to take IPC::Connection& as the first argument so the message-check macros can terminate the originating connection.Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.cpp (include)— Adds #include “WebFrameProxy.h” so the UI process can resolve the frame and read its authoritative URL.LayoutTests/http/tests/ipc/web-authenticator-get-assertion-spoofed-origin-crash.html— IPCTestingAPI test that sends GetAssertion with securityOrigin/topOrigin spoofed to https://evil.com and expects the WebContent process to be terminated by the new MESSAGE_CHECK (companion make-credential test mirrors it).LayoutTests/platform/wk2/TestExpectations— Skips the two origin-spoof crash tests in Debug, matching other IPC MESSAGE_CHECK tests that deliberately crash the WebContent process.
Background
WebAuthn origin binding — The Web Authentication API binds public-key credentials to a relying-party ID derived from the caller’s origin, which is what makes the credentials phishing-resistant. The UI/authenticator layer must know the true origin; if the origin can be forged, an attacker origin can request ceremonies impersonating a victim origin, defeating the core guarantee.
FrameInfoData.securityOrigin — A struct the WebContent process sends to the UI process describing a frame, including its securityOrigin and topOrigin. Because it is produced by a possibly-compromised renderer, its origin fields cannot be trusted as authoritative; the UI process must corroborate them against its own frame state.
parentOrigin (getAssertion) — GetAssertion accepts an optional parentOrigin used to evaluate cross-origin assertion policy for iframed WebAuthn. A spoofed parentOrigin misrepresents the embedding ancestor, so the fix requires it to match a real HTTP(S) ancestor frame’s origin.
WebFrameProxy authoritative URL — In the UI process, WebFrameProxy::webFrame(frameId) yields the proxy whose url() reflects the committed navigation the UI process itself tracks. Deriving expectedOrigin via SecurityOriginData::fromURLWithoutStrictOpaqueness(frame->url()) gives a trustworthy origin to compare against the renderer-supplied one.
MESSAGE_CHECK_COMPLETION_BASE — An IPC-validation macro variant that, on a failed predicate, invokes the provided completion handler (here returning an InvalidStateError exception) and terminates the offending connection. It is the enforcement mechanism ensuring a spoofing renderer is killed rather than served.
Vulnerability window
- Design — makeCredential/getAssertion forward the renderer-supplied FrameInfoData.securityOrigin and parentOrigin into the WebAuthn ceremony without cross-checking them against UI-process frame state.
- Threat — Under the compromised-renderer model, the origin fields are attacker-controlled, enabling an attacker page to request credential ceremonies impersonating another origin such as a bank.
- Report — Filed as bugs.webkit.org 311432 / rdar://172383653; originally landed on a Safari branch (305413.645) and rdar://176059128, indicating it was handled as a security fix before mainline.
- Fix — Both receivers resolve the true frame, derive expectedOrigin from frame->url(), and MESSAGE_CHECK the claimed securityOrigin (and parentOrigin against the ancestor chain), killing spoofing renderers.
- Regression tests — IPCTestingAPI tests send ceremonies with securityOrigin spoofed to evil.com and assert the WebContent process is terminated; Debug builds skip them since the crash is expected.
Proof of concept
The added test uses CoreIPC (IPCTestingAPI) to invoke WebAuthenticatorCoordinatorProxy.GetAssertion directly from a page whose real frame URL is the test origin, but with frameInfo.securityOrigin/topOrigin spoofed to https://evil.com. Pre-patch the UI process would honor the spoofed origin; post-patch expectedOrigin (derived from the real frame URL) differs, the MESSAGE_CHECK fails, and the WebContent process is terminated (the test’s expected outcome). The make-credential test is structurally identical for the creation path.
const spoofedOrigin = {
data: {
variantType: 'WebCore::SecurityOriginData::Tuple',
variant: { protocol: 'https', host: 'evil.com', port: {} }
}
};
CoreIPC.UI.WebAuthenticatorCoordinatorProxy.GetAssertion(IPC.webPageProxyID, {
frameID: IPC.frameID[0],
frameInfo: {
isMainFrame: true,
frameType: 0,
request: { /* ... ResourceRequest::RequestData ... */ },
securityOrigin: spoofedOrigin,
topOrigin: spoofedOrigin,
frameName: '',
frameID: IPC.frameID[0],
webPageProxyID: { optionalValue: IPC.webPageProxyID },
parentFrameID: {},
documentID: {},
certificateInfo: { trust: {} },
processID: { alias: 0 },
isFocused: false,
errorOccurred: false,
frameMetrics: { /* zeros */ }
},
options: {
challenge: { span: [0] },
timeout: {},
rpId: '',
allowCredentials: [],
userVerification: 1,
extensions: {}
},
mediation: 0,
parentOrigin: {}
});
// If we reach here, the origin validation didn't crash us.
setTimeout(() => {
console.log("FAIL: Should have crashed due to origin mismatch in GetAssertion");
testRunner?.notifyDone();
}, 1000);
Exploitation
- Prerequisite — Requires a compromised WebContent process able to emit crafted WebAuthenticatorCoordinatorProxy IPC (the tests rely on IPCTestingAPI, not present in production), so this is a post-renderer-compromise capability.
- Origin spoof — The attacker constructs a FrameInfoData with securityOrigin/topOrigin (and, for getAssertion, parentOrigin) set to a target relying-party origin such as a bank, while the actual frame is attacker-controlled.
- Ceremony hijack — The UI process previously drove the MakeCredential/GetAssertion ceremony under the spoofed origin, so an authenticator could create or produce an assertion the attacker attributes to the victim origin — an authentication/impersonation bypass rather than memory corruption.
- Limits — The check is only enforced for HTTP(S) frame URLs; non-HTTP schemes are exempt from the equality check, and successful abuse still depends on authenticator/user-verification flow. There is no memory-safety impact; the bug is confidentiality/integrity of the credential origin binding.
Detection & hunting
For defenders and SOC / detection engineers:
- Origin mismatch terminations —
- WebAuthn calls with origin != frame URL —
- parentOrigin without matching ancestor —
Audit directions
- Other WebAuthn IPC entry points —
- Renderer-supplied FrameInfoData consumers —
- Non-HTTP scheme handling —
- Ancestor-chain origin logic —