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 <npm@chromium.org>
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 <dtapuska@chromium.org>
Reviewed-by: Yi Gu <yigu@chromium.org>
Commit-Queue: Nicolás Peña <npm@chromium.org>
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