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 |
|---|---|---|
forcontent/browser/webid/request_service.cc |
modified | |
ifcontent/browser/webid/request_service.cc |
modified | |
TEST_Fcontent/browser/webid/request_registry_unittest.cc |
modified |
Files Changed
content/browser/webid/request_service.ccthird_party/blink/web_tests/virtual/fedcm-register/README.mdthird_party/blink/web_tests/VirtualTestSuitescontent/browser/webid/request_registry_unittest.cccontent/browser/webid/request_unittest.cc
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_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_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.
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.
Original Bug Report
FedCM: Potential bypass of .well-known enforcement via renderer-controlled flag
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in FedCM allows a compromised renderer to bypass mandatory origin authorization checks by spoofing the ‘from_idp_registration_api’ flag. This permits an attacker to display trusted browser UI impersonating arbitrary domains.
Affected files:
content/browser/webid/request_service.cccontent/browser/webid/config_fetcher.ccthird_party/blink/public/mojom/webid/federated_auth_request.mojomcontent/browser/webid/accounts_fetcher.cccontent/browser/webid/webid_utils.cc
Estimated timestamp from git blame: 2025-01-17
Summary
Federated Credential Management (FedCM) requires that an origin acting as an Identity Provider (IdP) authorize its configuration URLs via a /.well-known/web-identity file. This enforcement is bypassed if the IdP is registered via the browser’s IdP registration API. A logic flaw allows a compromised renderer to spoof this “registered” status, causing the browser to skip the mandatory well-known file check.
Vulnerability Details
The blink::mojom::IdentityProviderConfig struct, passed from the renderer to the browser in the FederatedAuthRequest::RequestToken IPC, contains a boolean field from_idp_registration_api. This flag is intended to signal that the configuration was retrieved from the browser’s trusted IdP registry.
In content/browser/webid/request_service.cc, the RequestToken method handles the incoming IPC. It contains logic to sanitize the provider list via MaybeAddRegisteredProviders (line 312), but this block is only executed if the kFedCmIdPRegistration feature is enabled (line 309). Since this feature is disabled by default, the browser proceeds with the raw, attacker-controlled IdentityProviderConfig structs provided in the IPC. Unlike the RequestUserInfo method (line 616), RequestToken fails to verify or clear the from_idp_registration_api flag when the feature is disabled.
The from_idp_registration_api flag propagates to ConfigFetcher::FetchRequest::force_skip_well_known_enforcement (line 806). In content/browser/webid/config_fetcher.cc, this flag causes ShouldSkipWellKnownEnforcementForIdp to return true (line 388). As a result:
- Network failures when fetching the
/.well-known/web-identityfile are ignored (line 101). - The mandatory check to ensure the config URL is listed in the well-known file’s
provider_urlslist is skipped (line 305).
Impact
An attacker with control over a renderer process can potentially perform UI spoofing on a primary security surface. By hosting a FedCM configuration file on a victim domain (e.g., via an upload path) and initiating a request with the spoofed flag, the attacker can cause the trusted native FedCM dialog to appear, identifying the victim origin as the IdP. This bypasses the fundamental requirement that the victim origin must opt-in to acting as an IdP.
If the experimental kFedCmLightweightMode is enabled, the browser may even populate the spoofed dialog with the user’s real accounts for the victim origin, as the browser trusts the (spoofed) registration status to skip the standard accounts fetch.
Potential Steps to Reproduce
- Compromise a renderer process.
- Identify a victim origin
https://victim.comthat does not host a/.well-known/web-identityfile. - Host a malicious FedCM config file at
https://victim.com/path/to/fedcm.json(e.g., via a file upload). - From the compromised renderer, send a
RequestTokenMojo IPC where theIdentityProviderConfighasconfig_url = "https://victim.com/path/to/fedcm.json"andfrom_idp_registration_api = true. - Observe that the browser fetches the malicious config, ignores the absence of the well-known file, and displays trusted native UI for
victim.com.
Note: These are potential steps based on code analysis; our current environment does not support running proof-of-concept code.
Suggested Fix
The browser process should never trust the from_idp_registration_api flag directly from a renderer. In content/browser/webid/request_service.cc, the RequestToken method should sanitize all input providers. Specifically, if IsIdPRegistrationEnabled() is false, the from_idp_registration_api flag must be forced to false for all providers. If the feature is enabled, the browser must verify that any provider claiming to be from the registration API is actually present in the trusted registry.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.