CVE-2026-87472
Overview
Background
- FedCM
- Chromium’s browser-mediated Federated Credential Management API that lets sites request identity tokens from identity providers without third-party cookies.
- IdP Registration API
- a FedCM sub-feature (gated by
FedCmIdPRegistration) where a browser-registered identity provider is used instead of an explicitly suppliedconfig_url. - `from_idp_registration_api`
- a boolean on
blink::mojom::IdentityProviderConfig, sent from the renderer, signaling that a get-request should resolve against a registered IdP rather than a configured URL. - `ReportBadMessage`
- a Mojo mechanism the browser process uses to reject malformed IPC and terminate the offending renderer for sending out-of-contract input.
Root Cause Analysis
The StartTokenRequest() handler in content/browser/webid/request_service.cc accepted renderer-supplied IdentityProviderConfig values and forwarded them into request creation without validating the from_idp_registration_api flag against its required invariants. A compromised or malicious renderer could set from_idp_registration_api to true even when the FedCmIdPRegistration feature was disabled, or set it true while also supplying a non-empty config_url, a combination the registration flow is not designed to handle. The browser process trusted this cross-process input as if it had been constrained by the renderer-side implementation, violating the rule that the privileged process must independently validate all Mojo input.
The fix iterates each provider and enforces two conditions: when the feature is disabled it sanitizes the flag back to false (tolerating benign test-config mismatches), and when the feature is enabled but config_url is non-empty it calls receivers_.ReportBadMessage("config_url must be empty for registered providers.") and aborts. This re-establishes the invariant that from_idp_registration_api implies both an enabled feature and an empty config_url before any request object is built.
from_idp_registration_api flag without re-validating it in the browser process; the fix adds an explicit browser-side check that resets the flag when the feature is off and rejects the message when a registered provider carries a config_url.Attack Path
- Compromised renderer
An attacker who controls a renderer process (e.g., via a prior bug) crafts a FedCM get-request through the
FederatedRequestMojo interface. - Forge registration flag
The attacker sets
provider->config->from_idp_registration_api = trueon anIdentityProviderConfigregardless of feature state orconfig_urlcontents. - Bypass renderer constraints
Because the browser’s
StartTokenRequest()did not re-check the flag, the inconsistent config (registration requested with a populatedconfig_url, or with the feature disabled) reached request construction. - Enter unintended code path The registered-IdP handling proceeds on input it was never designed to accept, driving FedCM logic into an out-of-contract state.
Impact Assessment
RequestService, consistent with the medium severity and “improper input validation” classification, and no memory-corruption primitive is evidenced by the diff. The precondition is a renderer able to send arbitrary blink::mojom FedCM messages.Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/webid/request_registry_unittest.cc |
modified | |
forcontent/browser/webid/request_service.cc |
modified | |
ifcontent/browser/webid/request_service.cc |
modified |
Files Changed
content/browser/webid/request_registry_unittest.cccontent/browser/webid/request_service.cccontent/browser/webid/request_unittest.ccthird_party/blink/web_tests/VirtualTestSuitesthird_party/blink/web_tests/virtual/fedcm-register/README.md
Audit Directions
- Renderer-supplied Mojo flagsAudit other
blink::mojomboolean and mode flags (likefrom_idp_registration_api) that select privileged code paths to confirm the browser process re-validates them rather than trusting renderer enforcement. - Feature-gated inputsWherever a
base::Featuregate exists only on the renderer side, verify the browser independently checksIsXEnabled()before honoring input that assumes the feature, and decide deliberately between sanitize-and-continue andReportBadMessage. - Cross-field invariantsLook for cases where one field’s value constrains another (here, a set registration flag requires an empty
config_url) and ensure those combined constraints are validated together at the trust boundary.
Patch
From 562273008c5efc49a07d8adbb66a4cb50ee98dd8 Mon Sep 17 00:00:00 2001 From: Nicolás Peña <[email protected]> Date: Wed, 12 Aug 2026 17:25:27 -0700 Subject: [PATCH] [FedCM] Validate from_idp_registration_api in StartTokenRequest() This CL adds security checks to ensure that from_idp_registration_api is only set to true when the FedCmIdPRegistration feature is enabled and the config_url is empty. If these conditions are violated, a bad message is reported. Unit tests are added to verify this behavior. Fixed: 514016678 Change-Id: Ica99d5522f0ed77c48928cfb27c16872ff764f02 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8221783 Auto-Submit: Nicolás Peña <[email protected]> Reviewed-by: Philip Rogers <[email protected]> Commit-Queue: Nicolás Peña <[email protected]> Reviewed-by: Yi Gu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1678418} --- diff --git a/content/browser/webid/request_registry_unittest.cc b/content/browser/webid/request_registry_unittest.cc index 793eb498..e7448f5 100644 --- a/content/browser/webid/request_registry_unittest.cc +++ b/content/browser/webid/request_registry_unittest.cc @@ -39,6 +39,8 @@ namespace content::webid { +namespace { + using ::testing::_; using ::testing::NiceMock; using ::testing::Return; @@ -46,8 +48,7 @@ using ApiPermissionStatus = FederatedIdentityApiPermissionContextDelegate::PermissionStatus; using blink::mojom::RegisterIdpStatus; - -namespace { +using MediationRequirement = ::password_manager::CredentialMediationRequirement; constexpr char kIdpUrl[] = "https://idp.example/"; @@ -489,4 +490,61 @@ run_loop.Run(); } +// Test that calling StartTokenRequest with from_idp_registration_api as true +// when the feature is disabled resolves with a normal error instead of a bad +// message. +TEST_F(RequestRegistryTest, StartTokenRequestFromIdpRegistrationDisabled) { + // Feature is disabled by default. + std::vector<blink::mojom::IdentityProviderGetParametersPtr> idp_get_params; + auto get_params = blink::mojom::IdentityProviderGetParameters::New(); + auto provider = blink::mojom::IdentityProviderRequestOptions::New(); + provider->config = blink::mojom::IdentityProviderConfig::New(); + provider->config->from_idp_registration_api = true; + get_params->providers.push_back(std::move(provider)); + idp_get_params.push_back(std::move(get_params)); + + EXPECT_CALL(*mock_permission_delegate_, RemoveIdpSigninStatusObserver(_)) + .WillOnce(Return()); + + mojo::Remote<blink::mojom::FederatedRequest> request_remote; + base::RunLoop run_loop; + request_service_remote_->StartTokenRequest( + std::move(idp_get_params), MediationRequirement::kOptional, + request_remote.BindNewPipeAndPassReceiver(), + base::BindLambdaForTesting( + [&run_loop]( + blink::mojom::FederatedRequestService::StartTokenRequestResult + result) { + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(blink::mojom::RequestTokenStatus::kError, + result.error()->status); + run_loop.Quit(); + })); + run_loop.Run(); +} + +// Test that calling StartTokenRequest with from_idp_registration_api as true +// and a non-empty config_url triggers a Bad Message. +TEST_F(RequestRegistryTest, StartTokenRequestFromIdpRegistrationNonEmptyUrl) { + feature_list_.InitAndEnableFeature(features::kFedCmIdPRegistration); + + std::vector<blink::mojom::IdentityProviderGetParametersPtr> idp_get_params; + auto get_params = blink::mojom::IdentityProviderGetParameters::New(); + auto provider = blink::mojom::IdentityProviderRequestOptions::New(); + provider->config = blink::mojom::IdentityProviderConfig::New(); + provider->config->from_idp_registration_api = true; + provider->config->config_url = GURL(kIdpUrl); + get_params->providers.push_back(std::move(provider)); + idp_get_params.push_back(std::move(get_params)); + + mojo::test::BadMessageObserver bad_message_observer; + mojo::Remote<blink::mojom::FederatedRequest> request_remote; + request_service_remote_->StartTokenRequest( + std::move(idp_get_params), MediationRequirement::kOptional, + request_remote.BindNewPipeAndPassReceiver(), base::DoNothing()); + + EXPECT_EQ("config_url must be empty for registered providers.", + bad_message_observer.WaitForBadMessage()); +} + } // namespace content::webid diff --git a/content/browser/webid/request_service.cc b/content/browser/webid/request_service.cc index 8a0aefa..5a9109d 100644 --- a/content/browser/webid/request_service.cc +++ b/content/browser/webid/request_service.cc @@ -197,6 +197,24 @@ return; } + for (auto& provider : idp_get_params[0]->providers) { + if (provider->config->from_idp_registration_api) { + if (!IsIdPRegistrationEnabled()) { + // In layout and web platform tests, features with "status: test" (like + // FedCmIdPRegistration) are enabled by default on the renderer-side, + // but the corresponding browser-side base::Feature may be disabled. + // To prevent mismatches in benign test configurations from terminating + // the renderer, we sanitize/reset from_idp_registration_api to false + // instead of reporting a bad message. + provider->config->from_idp_registration_api = false; + } else if (!provider->config->config_url.is_empty()) { + receivers_.ReportBadMessage( + "config_url must be empty for registered providers."); + return; + } + } + } + RenderFrameHost& rfh = render_frame_host(); auto new_request = std::make_unique<Request>(&rfh, *this); new_request->BindReceiver(std::move(request_receiver)); diff --git a/content/browser/webid/request_unittest.cc b/content/browser/webid/request_unittest.cc index 0359465..775d244 100644 --- a/content/browser/webid/request_unittest.cc +++ b/content/browser/webid/request_unittest.cc @@ -2185,6 +2185,7 @@ config.idp_info[idp_config_url].well_known = { {kWellKnownMismatchConfigUrl}, {ParseStatus::kSuccess, net::HTTP_OK}}; RequestParameters requestParameters = kDefaultRequestParameters; + requestParameters.identity_providers[0].provider = ""; requestParameters.identity_providers[0].from_idp_registration_api = true; // Need to simulate there is actually a registered IdP, or the call will fail. diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites index ed4b2f9a..3738572 100644 --- a/third_party/blink/web_tests/VirtualTestSuites +++ b/third_party/blink/web_tests/VirtualTestSuites @@ -1156,7 +1156,7 @@ ], "exclusive_tests": "ALL", "args": [ - "--enable-features=FedCmIdPregistration" + "--enable-features=FedCmIdPRegistration" ], "owners": [ "[email protected]", diff --git a/third_party/blink/web_tests/virtual/fedcm-register/README.md b/third_party/blink/web_tests/virtual/fedcm-register/README.md index 8a5b56c..8d5646d 100644 --- a/third_party/blink/web_tests/virtual/fedcm-register/README.md +++ b/third_party/blink/web_tests/virtual/fedcm-register/README.md @@ -1,5 +1,6 @@ # FedCmRegister + This suite runs the tests in wpt/credential-management/fedcm-register/ with -`--enable-features=FedCmIdPregistration`. +`--enable-features=FedCmIdPRegistration`. See crbug.com/40252825.
Regression Test / PoC
diff --git a/content/browser/webid/request_registry_unittest.cc b/content/browser/webid/request_registry_unittest.cc
index 793eb498..e7448f5 100644
--- a/content/browser/webid/request_registry_unittest.cc
+++ b/content/browser/webid/request_registry_unittest.cc
@@ -39,6 +39,8 @@
namespace content::webid {
+namespace {
+
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
@@ -46,8 +48,7 @@
using ApiPermissionStatus =
FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using blink::mojom::RegisterIdpStatus;
-
-namespace {
+using MediationRequirement = ::password_manager::CredentialMediationRequirement;
constexpr char kIdpUrl[] = "https://idp.example/";
@@ -489,4 +490,61 @@
run_loop.Run();
}
+// Test that calling StartTokenRequest with from_idp_registration_api as true
+// when the feature is disabled resolves with a normal error instead of a bad
+// message.
+TEST_F(RequestRegistryTest, StartTokenRequestFromIdpRegistrationDisabled) {
+ // Feature is disabled by default.
+ std::vector<blink::mojom::IdentityProviderGetParametersPtr> idp_get_params;
+ auto get_params = blink::mojom::IdentityProviderGetParameters::New();
+ auto provider = blink::mojom::IdentityProviderRequestOptions::New();
+ provider->config = blink::mojom::IdentityProviderConfig::New();
+ provider->config->from_idp_registration_api = true;
+ get_params->providers.push_back(std::move(provider));
+ idp_get_params.push_back(std::move(get_params));
+
+ EXPECT_CALL(*mock_permission_delegate_, RemoveIdpSigninStatusObserver(_))
+ .WillOnce(Return());
+
+ mojo::Remote<blink::mojom::FederatedRequest> request_remote;
+ base::RunLoop run_loop;
+ request_service_remote_->StartTokenRequest(
+ std::move(idp_get_params), MediationRequirement::kOptional,
+ request_remote.BindNewPipeAndPassReceiver(),
+ base::BindLambdaForTesting(
+ [&run_loop](
+ blink::mojom::FederatedRequestService::StartTokenRequestResult
+ result) {
+ EXPECT_FALSE(result.has_value());
+ EXPECT_EQ(blink::mojom::RequestTokenStatus::kError,
+ result.error()->status);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+// Test that calling StartTokenRequest with from_idp_registration_api as true
+// and a non-empty config_url triggers a Bad Message.
+TEST_F(RequestRegistryTest, StartTokenRequestFromIdpRegistrationNonEmptyUrl) {
+ feature_list_.InitAndEnableFeature(features::kFedCmIdPRegistration);
+
+ std::vector<blink::mojom::IdentityProviderGetParametersPtr> idp_get_params;
+ auto get_params = blink::mojom::IdentityProviderGetParameters::New();
+ auto provider = blink::mojom::IdentityProviderRequestOptions::New();
+ provider->config = blink::mojom::IdentityProviderConfig::New();
+ provider->config->from_idp_registration_api = true;
+ provider->config->config_url = GURL(kIdpUrl);
+ get_params->providers.push_back(std::move(provider));
+ idp_get_params.push_back(std::move(get_params));
+
+ mojo::test::BadMessageObserver bad_message_observer;
+ mojo::Remote<blink::mojom::FederatedRequest> request_remote;
+ request_service_remote_->StartTokenRequest(
+ std::move(idp_get_params), MediationRequirement::kOptional,
+ request_remote.BindNewPipeAndPassReceiver(), base::DoNothing());
+
+ EXPECT_EQ("config_url must be empty for registered providers.",
+ bad_message_observer.WaitForBadMessage());
+}
+
} // namespace content::webid
diff --git a/content/browser/webid/request_unittest.cc b/content/browser/webid/request_unittest.cc
index 0359465..775d244 100644
--- a/content/browser/webid/request_unittest.cc
+++ b/content/browser/webid/request_unittest.cc
@@ -2185,6 +2185,7 @@
config.idp_info[idp_config_url].well_known = {
{kWellKnownMismatchConfigUrl}, {ParseStatus::kSuccess, net::HTTP_OK}};
RequestParameters requestParameters = kDefaultRequestParameters;
+ requestParameters.identity_providers[0].provider = "";
requestParameters.identity_providers[0].from_idp_registration_api = true;
// Need to simulate there is actually a registered IdP, or the call will fail.
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index ed4b2f9a..3738572 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -1156,7 +1156,7 @@
],
"exclusive_tests": "ALL",
"args": [
- "--enable-features=FedCmIdPregistration"
+ "--enable-features=FedCmIdPRegistration"
],
"owners": [
"[email protected]",
diff --git a/third_party/blink/web_tests/virtual/fedcm-register/README.md b/third_party/blink/web_tests/virtual/fedcm-register/README.md
index 8a5b56c..8d5646d 100644
--- a/third_party/blink/web_tests/virtual/fedcm-register/README.md
+++ b/third_party/blink/web_tests/virtual/fedcm-register/README.md
@@ -1,5 +1,6 @@
# FedCmRegister
+
This suite runs the tests in wpt/credential-management/fedcm-register/ with
-`--enable-features=FedCmIdPregistration`.
+`--enable-features=FedCmIdPRegistration`.
See crbug.com/40252825.