CVE-2026-87608
Overview
Background
- FedCM
- the Federated Credential Management API, a browser feature that lets a Relying Party sign users in through an Identity Provider (IdP) while the browser mediates the exchange.
- `Set-Login` header
- an HTTP response header an IdP sends to tell the browser to persistently record origin-keyed sign-in status (logged-in / logged-out) for that IdP origin.
- `IdentityUrlLoaderThrottle`
- a
blink::URLLoaderThrottleincontent/common/webid/that inspects IdP network responses and acts onSet-Loginheaders as they arrive. - `net::IsCertStatusError` / `cert_status`
cert_statusis the bitfield of TLS certificate problems recorded for a response, andnet::IsCertStatusErrorreturns true when that bitfield contains a fatal certificate error (e.g.CERT_STATUS_DATE_INVALID).
Root Cause Analysis
The Set-Login handling path in content/common/webid/identity_url_loader_throttle.cc (reached via WillProcessResponse, the method the regression test invokes; the enclosing function name is not present in the hunk header, so this is (inferred)) validated that the response origin was potentially trustworthy but never consulted response_head.cert_status. Because a user who clicks through the certificate-error interstitial still receives the response body and headers, an HTTPS connection carrying a self-signed or otherwise invalid certificate would have its Set-Login header processed as if the TLS identity were sound, violating the invariant that security-sensitive, persistently-stored state must only be accepted over a connection whose certificate actually validated.
The fix adds an early return when net::IsCertStatusError(response_head.cert_status) is true — placed before the Set-Login processing logic — so a response over a cert-errored connection is dropped without side effects. The guard is bypassed only when the switches::kIgnoreCertificateErrors command-line switch is set, preserving test/dev workflows while closing the production path. This TIGHTENS the check: a header that previously would have been honored is now rejected.
Set-Login processing on origin trustworthiness alone while ignoring whether the TLS certificate for that connection actually validated; the fix adds the missing net::IsCertStatusError(response_head.cert_status) guard so responses over cert-errored connections early-return and never persist sign-in status.Attack Path
- Position as MITM
An attacker intercepts the victim’s HTTPS connection to an IdP origin (e.g.
https://accounts.idp.example/) and presents a self-signed or otherwise invalid TLS certificate. - User clicks past the interstitial
The victim proceeds through Chrome’s certificate-error warning, so the response is delivered to the renderer/network stack with
cert_statusset to a fatal error such asnet::CERT_STATUS_DATE_INVALID. - Inject `Set-Login`
The attacker’s forged response includes a
Set-Loginheader; pre-patch,IdentityUrlLoaderThrottlereached the header-processing code because it only checked origin trustworthiness, notcert_status. - Persist forged sign-in state
The header causes persistent origin-keyed FedCM sign-in status for the IdP to be written under attacker control, enabling cross-site denial of service or state confusion
(inferred)from the advisory’s stated impact.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/common/webid/identity_url_loader_throttle_unittest.cc |
modified |
Files Changed
content/common/webid/identity_url_loader_throttle_unittest.cccontent/common/DEPScontent/common/webid/identity_url_loader_throttle.cccontent/common/BUILD.gn
Audit Directions
- Trustworthiness without cert validationLook for other response-header or response-body handlers that gate on origin trustworthiness (
IsOriginPotentiallyTrustworthy/ potentially-trustworthy checks) but never inspectresponse_head.cert_status, since click-through interstitials mean a trustworthy scheme does not imply a valid certificate. - Persistent state from network responsesAudit code paths that write persistent or origin-keyed browser state (sign-in status, permissions, storage flags) from IdP or third-party responses, confirming they early-return on
net::IsCertStatusErrorlike other security-sensitive headers do. - Throttle orderingVerify that any newly added guards in
URLLoaderThrottlesubclasses run before the sink they protect, and that thekIgnoreCertificateErrorsbypass is the only intended exception.
Patch
From 4906428f67d6d47d3cb5341b7517389e889181ed Mon Sep 17 00:00:00 2001 From: Nicolás Peña <[email protected]> Date: Thu, 06 Aug 2026 07:53:37 -0700 Subject: [PATCH] [FedCM] Reject Set-Login headers on connections with cert errors IdentityUrlLoaderThrottle handles Set-Login response headers to set persistent origin-keyed IdP sign-in status. Currently, it checks whether the origin is potentially trustworthy, but does not check whether the connection has TLS certificate errors. An attacker MITM-ing an HTTPS connection with a self-signed or invalid certificate could cause Set-Login headers to be processed if the user clicked past the certificate error interstitial. This could persist origin-keyed sign-in status, leading to cross-site denial of service or state confusion. This CL early-returns in IdentityUrlLoaderThrottle when net::IsCertStatusError(response_head.cert_status) is true, matching the pattern used by other security-sensitive response headers. Bug: 540072282 Change-Id: I4693599760fa036b0c1194e84816e900f7bdd205 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8198686 Reviewed-by: Dave Tapuska <[email protected]> Reviewed-by: Yi Gu <[email protected]> Commit-Queue: Nicolás Peña <[email protected]> Cr-Commit-Position: refs/heads/main@{#1674972} --- diff --git a/content/common/webid/identity_url_loader_throttle.cc b/content/common/webid/identity_url_loader_throttle.cc index 7f396480..c6e479e 100644 --- a/content/common/webid/identity_url_loader_throttle.cc +++ b/content/common/webid/identity_url_loader_throttle.cc @@ -8,14 +8,17 @@ #include <string_view> #include "base/auto_reset.h" +#include "base/command_line.h" #include "base/functional/bind.h" #include "base/strings/string_split.h" #include "base/task/sequenced_task_runner.h" #include "base/time/time.h" +#include "components/network_session_configurator/common/network_switches.h" #include "content/common/features.h" #include "content/public/common/content_client.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" +#include "net/cert/cert_status_flags.h" #include "net/http/http_response_headers.h" #include "net/http/structured_headers.h" #include "services/data_decoder/public/cpp/data_decoder.h" @@ -139,6 +142,12 @@ return; } + if (net::IsCertStatusError(response_head.cert_status) && + !base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kIgnoreCertificateErrors)) { + return; + } + // TODO(crbug.com/40236764): // - Limit to toplevel frames // - Decide whether to limit to same-origin diff --git a/content/common/webid/identity_url_loader_throttle_unittest.cc b/content/common/webid/identity_url_loader_throttle_unittest.cc index da7a8475..b972da46 100644 --- a/content/common/webid/identity_url_loader_throttle_unittest.cc +++ b/content/common/webid/identity_url_loader_throttle_unittest.cc @@ -11,6 +11,7 @@ #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" #include "content/public/common/web_identity.h" +#include "net/cert/cert_status_flags.h" #include "net/http/http_response_headers.h" #include "net/http/structured_headers.h" #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" @@ -354,4 +355,29 @@ EXPECT_TRUE(result_item->item.is_integer()); } +TEST_F(IdentityUrlLoaderThrottleTest, CertError) { + TestDelegate delegate; + std::unique_ptr<blink::URLLoaderThrottle> throttle = + MaybeCreateIdentityUrlLoaderThrottle(CreateCallback(), + CreateParseCallback()); + ASSERT_NE(nullptr, throttle); + throttle->set_delegate(&delegate); + + network::ResourceRequest request; + request.url = GURL("https://accounts.idp.example/"); + bool defer = false; + + throttle->WillStartRequest(&request, &defer); + EXPECT_FALSE(defer); + + network::mojom::URLResponseHead response_head; + response_head.cert_status = net::CERT_STATUS_DATE_INVALID; + response_head.headers = net::HttpResponseHeaders::TryToCreate( + "HTTP/1.1 200 OK\nSet-Login: logged-in\n"); + throttle->WillProcessResponse(request.url, &response_head, &defer); + EXPECT_FALSE(defer); + + EXPECT_EQ(0, cb_num_calls_); +} + } // namespace content diff --git a/content/common/BUILD.gn b/content/common/BUILD.gn index 264cdb4d..6db21bb 100644 --- a/content/common/BUILD.gn +++ b/content/common/BUILD.gn @@ -224,6 +224,7 @@ "//build:branding_buildflags", "//components/discardable_memory/common", "//components/input", + "//components/network_session_configurator/common", "//components/services/filesystem/public/mojom", "//components/tracing:tracing_config", "//content:content_resources", diff --git a/content/common/DEPS b/content/common/DEPS index fff9622..6379910 100644 --- a/content/common/DEPS +++ b/content/common/DEPS @@ -2,6 +2,7 @@ "-storage/browser", "+components/discardable_memory/common", + "+components/network_session_configurator/common", "+components/viz/common", "+device/base/synchronization", "+services/data_decoder/public/cpp",
Regression Test / PoC
diff --git a/content/common/webid/identity_url_loader_throttle_unittest.cc b/content/common/webid/identity_url_loader_throttle_unittest.cc
index da7a8475..b972da46 100644
--- a/content/common/webid/identity_url_loader_throttle_unittest.cc
+++ b/content/common/webid/identity_url_loader_throttle_unittest.cc
@@ -11,6 +11,7 @@
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/web_identity.h"
+#include "net/cert/cert_status_flags.h"
#include "net/http/http_response_headers.h"
#include "net/http/structured_headers.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
@@ -354,4 +355,29 @@
EXPECT_TRUE(result_item->item.is_integer());
}
+TEST_F(IdentityUrlLoaderThrottleTest, CertError) {
+ TestDelegate delegate;
+ std::unique_ptr<blink::URLLoaderThrottle> throttle =
+ MaybeCreateIdentityUrlLoaderThrottle(CreateCallback(),
+ CreateParseCallback());
+ ASSERT_NE(nullptr, throttle);
+ throttle->set_delegate(&delegate);
+
+ network::ResourceRequest request;
+ request.url = GURL("https://accounts.idp.example/");
+ bool defer = false;
+
+ throttle->WillStartRequest(&request, &defer);
+ EXPECT_FALSE(defer);
+
+ network::mojom::URLResponseHead response_head;
+ response_head.cert_status = net::CERT_STATUS_DATE_INVALID;
+ response_head.headers = net::HttpResponseHeaders::TryToCreate(
+ "HTTP/1.1 200 OK\nSet-Login: logged-in\n");
+ throttle->WillProcessResponse(request.url, &response_head, &defer);
+ EXPECT_FALSE(defer);
+
+ EXPECT_EQ(0, cb_num_calls_);
+}
+
} // namespace content