CVE-2026-87561
Overview
Background
- `remoteDesktopClientOverride`
- A WebAuthn request extension that lets a trusted remote-desktop client tell the browser to substitute a different origin (the origin of the machine being remoted into) when building the ceremony’s
clientDataJSON. - `clientDataJSON`
- The browser-generated, signed-over blob that records the origin the WebAuthn ceremony was bound to; forging its origin lets an assertion or registration be attributed to a site the caller does not actually control.
- `OriginMayUseRemoteDesktopClientOverride`
- A
WebAuthenticationDelegatepolicy hook that returns true only whencaller_originis on the device-level enterprise allowlist permitted to use the override extension. - Embedder-delegated validation
- For certain callers (e.g.
chrome-extension://origins),ValidateDomainAndRelyingPartyIDhands origin/RP-ID validation to the embedder and returns early instead of running the standard in-function checks.
Root Cause Analysis
In WebAuthRequestSecurityCheckerImpl::ValidateDomainAndRelyingPartyID, the OriginMayUseRemoteDesktopClientOverride policy check was performed inline near the end of the function, immediately before relying_party_origin was set to the renderer-supplied remote_desktop_client_override_origin. However, callers whose validation is delegated to the embedder — such as chrome-extension:// origins — hit an earlier return nullptr success path (in the extension-handling block) that ran before that policy check. Consequently the invariant “an origin may only override the RP origin if it is on the device-level allowlist” was never evaluated for those callers, so an extension could supply an arbitrary remoteDesktopClientOverride origin and have it flow into the signed clientDataJSON.
The fix hoists the has_value() + OriginMayUseRemoteDesktopClientOverride guard to the very top of the function, so it runs before every early-return path, and replaces the now-redundant inline check with remote_desktop_client_override_origin.value_or(caller_origin). Because the allowlist is now consulted before any delegation return, an unauthorized caller receives REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED and no override is applied.
Attack Path
- Obtain a delegated-validation origin
An attacker controls or induces a
chrome-extension://origin (a caller type whose origin/RP-ID validation is delegated to the embedder) to initiate a WebAuthncreate/getceremony. - Supply the override
The caller includes a
remoteDesktopClientOverrideextension carrying an arbitrary attacker-chosen origin, e.g.https://example.com, despite not being on the device-level allowlist. - Bypass the gate
ValidateDomainAndRelyingPartyIDtakes the extension/embedder early-return path before reaching the inlineOriginMayUseRemoteDesktopClientOverridecheck, so the unauthorized override is never rejected. - Origin substitution
relying_party_originis set from the renderer-supplied override and written into the browser-generatedclientDataJSON, attributing the ceremony to an origin the caller is not authorized to claim (inferred as the downstream effect from the commit message; the diff shows only the gate, not the clientDataJSON writer).
Impact Assessment
clientDataJSON of a WebAuthn assertion or registration, an incorrect-authorization flaw in the Web Authentication component. The commit itself notes that OverrideCallerOriginAndRelyingPartyIdValidation severely constrains the set of origins an extension could actually claim, limiting real-world exploitability, and the preconditions include an extension origin with extensions permitted in the browser context. No exploited-in-the-wild status or severity rating is provided in the advisory text supplied.Files Changed
content/browser/webauth/authenticator_impl_unittest.cccontent/browser/webauth/webauth_request_security_checker_impl.cc
Audit Directions
- Guard-after-early-returnLook for security/authorization checks placed inline mid-function when the same function has earlier
returnpaths (delegation, platform#ifbranches, error shortcuts) that would skip them; the guard belongs before every exit. - Renderer-supplied origin substitutionAudit every consumer of untrusted, renderer-provided origins (
remote_desktop_client_override_origin, RP-ID claims, override extensions) to confirm each is authorized against a policy/allowlist before it reaches signed or trust-bearing output likeclientDataJSON. - Caller-type-specific bypassesReview checks that assume a single caller shape (web origins) but are reachable by alternate caller types (
chrome-extension://, embedder-delegated, immediate-mediation) that follow divergent code paths through the same validation routine.
Patch
From 7f59498d7097ca3905b4401463347e139bc905c5 Mon Sep 17 00:00:00 2001 From: Martin Kreichgauer <[email protected]> Date: Fri, 31 Jul 2026 14:20:44 -0700 Subject: [PATCH] webauthn: gate remoteDesktopClientOverride for all caller types ValidateDomainAndRelyingPartyID returned early for callers whose origin/RP-ID validation is delegated to the embedder (e.g. chrome-extension:// origins) without first evaluating OriginMayUseRemoteDesktopClientOverride against the caller. As a result, such a caller could supply the remoteDesktopClientOverride extension and have its origin written into the browser-generated clientDataJSON regardless of whether the caller origin was on the device-level allowlist. In practice, OverrideCallerOriginAndRelyingPartyIdValidation severely limits the set of origins an extension could claim by doing this, but nevertheless it is a good idea to tighten this up. Hoist the policy check to the start of the function so that it runs before every early-return path, and replace the now redundant inline check with a value_or() for the relying-party-origin assignment. BUG=499217288 TAG=agy CONV=b7e1564f-5289-478d-bba4-48195f9673f6 Change-Id: I4a423db7677e3abd875219621daec6ca9b95c884 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8140023 Reviewed-by: Ken Buchanan <[email protected]> Commit-Queue: Martin Kreichgauer <[email protected]> Cr-Commit-Position: refs/heads/main@{#1672106} --- diff --git a/content/browser/webauth/webauth_request_security_checker_impl.cc b/content/browser/webauth/webauth_request_security_checker_impl.cc index 8e49065..a40d72b 100644 --- a/content/browser/webauth/webauth_request_security_checker_impl.cc +++ b/content/browser/webauth/webauth_request_security_checker_impl.cc @@ -159,6 +159,23 @@ RequestType request_type, const std::optional<url::Origin>& remote_desktop_client_override_origin, base::OnceCallback<void(blink::mojom::AuthenticatorStatus)> callback) { + if (remote_desktop_client_override_origin.has_value()) { + // SECURITY: `remote_desktop_client_override_origin` comes from the renderer + // process and should not be trusted by default. We only allow its use when + // the `caller_origin` is explicitly allowlisted through device level + // enterprise policy. + if (!GetContentClient() + ->browser() + ->GetWebAuthenticationDelegate() + ->OriginMayUseRemoteDesktopClientOverride( + render_frame_host_->GetBrowserContext(), caller_origin)) { + std::move(callback).Run( + blink::mojom::AuthenticatorStatus:: + REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED); + return nullptr; + } + } + #if !BUILDFLAG(IS_ANDROID) // Extensions are not supported on Android. if (GetContentClient() @@ -188,24 +205,8 @@ return nullptr; } - url::Origin relying_party_origin = caller_origin; - if (remote_desktop_client_override_origin.has_value()) { - // SECURITY: `remote_desktop_client_override_origin` comes from the renderer - // process and should not be trusted by default. We only allow its use when - // the `caller_origin` is explicitly allowlisted through device level - // enterprise policy. - if (!GetContentClient() - ->browser() - ->GetWebAuthenticationDelegate() - ->OriginMayUseRemoteDesktopClientOverride( - render_frame_host_->GetBrowserContext(), caller_origin)) { - std::move(callback).Run( - blink::mojom::AuthenticatorStatus:: - REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED); - return nullptr; - } - relying_party_origin = remote_desktop_client_override_origin.value(); - } + url::Origin relying_party_origin = + remote_desktop_client_override_origin.value_or(caller_origin); if (webauthn::OriginIsAllowedToClaimRelyingPartyId(relying_party_id, relying_party_origin)) { diff --git a/content/browser/webauth/authenticator_impl_unittest.cc b/content/browser/webauth/authenticator_impl_unittest.cc index 5e9f2feb..6ef94c5 100644 --- a/content/browser/webauth/authenticator_impl_unittest.cc +++ b/content/browser/webauth/authenticator_impl_unittest.cc @@ -3249,6 +3249,40 @@ } } +// A Chrome extension should not be authorized to use the +// remoteDesktopClientOverride request extension. +TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest, + ExtensionCallerOrigin) { + static const std::string kExtensionOrigin = + base::StrCat({kExtensionScheme, "://abcdefg"}); + test_client_.GetTestWebAuthenticationDelegate()->permit_extensions = true; + + { + PublicKeyCredentialCreationOptionsPtr options = + GetTestPublicKeyCredentialCreationOptions(); + options->relying_party.id = kExampleRpId; + options->remote_desktop_client_override = RemoteDesktopClientOverride::New( + url::Origin::Create(GURL(kExampleOrigin)), true); + EXPECT_EQ( + AuthenticatorMakeCredential(std::move(options)).status, + AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED); + } + + { + PublicKeyCredentialRequestOptionsPtr options = + GetTestPublicKeyCredentialRequestOptions(); + options->relying_party_id = kExampleRpId; + options->extensions->remote_desktop_client_override = + RemoteDesktopClientOverride::New( + url::Origin::Create(GURL(kExampleOrigin)), true); + ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( + options->allow_credentials[0].id, kExtensionOrigin)); + EXPECT_EQ( + AuthenticatorGetAssertion(std::move(options)).status, + AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED); + } +} + TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest, GetAssertionImmediateMediation) { // Verify that an authorized origin may not use the extension with immediate
Regression Test / PoC
diff --git a/content/browser/webauth/authenticator_impl_unittest.cc b/content/browser/webauth/authenticator_impl_unittest.cc
index 5e9f2feb..6ef94c5 100644
--- a/content/browser/webauth/authenticator_impl_unittest.cc
+++ b/content/browser/webauth/authenticator_impl_unittest.cc
@@ -3249,6 +3249,40 @@
}
}
+// A Chrome extension should not be authorized to use the
+// remoteDesktopClientOverride request extension.
+TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest,
+ ExtensionCallerOrigin) {
+ static const std::string kExtensionOrigin =
+ base::StrCat({kExtensionScheme, "://abcdefg"});
+ test_client_.GetTestWebAuthenticationDelegate()->permit_extensions = true;
+
+ {
+ PublicKeyCredentialCreationOptionsPtr options =
+ GetTestPublicKeyCredentialCreationOptions();
+ options->relying_party.id = kExampleRpId;
+ options->remote_desktop_client_override = RemoteDesktopClientOverride::New(
+ url::Origin::Create(GURL(kExampleOrigin)), true);
+ EXPECT_EQ(
+ AuthenticatorMakeCredential(std::move(options)).status,
+ AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED);
+ }
+
+ {
+ PublicKeyCredentialRequestOptionsPtr options =
+ GetTestPublicKeyCredentialRequestOptions();
+ options->relying_party_id = kExampleRpId;
+ options->extensions->remote_desktop_client_override =
+ RemoteDesktopClientOverride::New(
+ url::Origin::Create(GURL(kExampleOrigin)), true);
+ ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration(
+ options->allow_credentials[0].id, kExtensionOrigin));
+ EXPECT_EQ(
+ AuthenticatorGetAssertion(std::move(options)).status,
+ AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED);
+ }
+}
+
TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest,
GetAssertionImmediateMediation) {
// Verify that an authorized origin may not use the extension with immediate