← WebKit Silent-Fix Report — 2026-W25

315ac3073a  Message check security origin during webauthn calls

severity high class CrossOrigin confidence 0.80 WebKit WebAuthn IPC exploitable-grade
Pascoe Tue Jun 16 13:57:48 2026 -0700 full: 315ac3073a08ffac128b256244e4ce12d049d4d1 bug report ↗ view on GitHub ↗
Primitive: spoofed security origin in WebAuthenticator IPC
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

  1. Design — makeCredential/getAssertion forward the renderer-supplied FrameInfoData.securityOrigin and parentOrigin into the WebAuthn ceremony without cross-checking them against UI-process frame state.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

Before / after

Loading diff…