Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in FedCM
DescriptionMemory corruption in FedCM
ComponentFedCM
Sub-componentFedCM
Bug ClassMemory Corruption
Tracker507227196
Fix commit316307c398a4 (chromium/src) +65/-16
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
content/common/webid/identity_url_loader_throttle.cc
modified
TEST_F
content/common/webid/identity_url_loader_throttle_unittest.cc
modified

Files Changed

  • content/common/webid/identity_url_loader_throttle_unittest.cc
  • content/common/webid/identity_url_loader_throttle.h
  • content/browser/loader/url_loader_throttles.cc
  • content/shell/renderer/shell_content_renderer_client.cc
  • content/public/common/web_identity.h
  • chrome/renderer/url_loader_throttle_provider_impl.cc
  • content/common/webid/identity_url_loader_throttle.cc
  • headless/lib/renderer/headless_content_renderer_client.cc
From 316307c398a462e0e969727307b0404200016492 Mon Sep 17 00:00:00 2001
From: Yi Gu <yigu@chromium.org>
Date: Mon, 14 Sep 2026 21:20:33 -0700
Subject: [PATCH] [FedCM] Fix Set-Login same-site bypass in SXG cert fetches

When creating throttles for top-level navigation, the getter callback
reads the destination from the navigation request (which is kDocument).
Signed Exchange (SXG) certificate fetches reuse this getter to generate
throttles, causing the identity throttle to use the navigation's
destination instead of the cert fetch's destination, thereby skipping
same-site enforcement.

See NavigationURLLoaderImpl::CreateURLLoaderThrottles() (which always
uses resource_request()) and NavigationURLLoaderImpl::
CreateSignedExchangeRequestHandler (which uses the former function).

This fix instead requests request.destination in
IdentityUrlLoaderThrottle::WillStartRequest and passes it to the login
status check, which is guaranteed to be correct. This ensures that cert
fetches are correctly identified as non-document requests and same-site
constraints are enforced.

Fixed: 507227196
Change-Id: I55b637a784e0727c251db0202f02d975c349f961
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800003
Reviewed-by: Nicolás Peña <npm@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Commit-Queue: Yi Gu <yigu@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697790}
---
diff --git a/chrome/renderer/url_loader_throttle_provider_impl.cc b/chrome/renderer/url_loader_throttle_provider_impl.cc
index d49edab1..c6735f9 100644
--- a/chrome/renderer/url_loader_throttle_provider_impl.cc
+++ b/chrome/renderer/url_loader_throttle_provider_impl.cc
@@ -283,6 +283,7 @@
             [](const blink::LocalFrameToken& token,
                const scoped_refptr<base::SequencedTaskRunner>
                    main_thread_task_runner,
+               network::mojom::RequestDestination destination,
                const std::optional<url::Origin>& initiator,
                const url::Origin& idp_origin,
                blink::mojom::IdpSigninStatus status) {
diff --git a/content/browser/loader/url_loader_throttles.cc b/content/browser/loader/url_loader_throttles.cc
index 72dabf6..b5e38a8 100644
--- a/content/browser/loader/url_loader_throttles.cc
+++ b/content/browser/loader/url_loader_throttles.cc
@@ -117,9 +117,17 @@
   }
 
   if (auto throttle = MaybeCreateIdentityUrlLoaderThrottle(
-          base::BindRepeating(webid::SetIdpSigninStatus,
-                              browser_context->GetWeakPtr(),
-                              request.destination, frame_tree_node_id),
+          base::BindRepeating(
+              [](base::WeakPtr<content::BrowserContext> context,
+                 FrameTreeNodeId ftn_id,
+                 network::mojom::RequestDestination destination,
+                 const std::optional<url::Origin>& initiator,
+                 const url::Origin& idp_origin,
+                 blink::mojom::IdpSigninStatus status) {
+                webid::SetIdpSigninStatus(context, destination, ftn_id,
+                                          initiator, idp_origin, status);
+              },
+              browser_context->GetWeakPtr(), frame_tree_node_id),
           GetSetLoginHeaderDataDecoderParser())) {
     throttles.push_back(std::move(throttle));
   }
diff --git a/content/common/webid/identity_url_loader_throttle.cc b/content/common/webid/identity_url_loader_throttle.cc
index 8caa975..f6384290 100644
--- a/content/common/webid/identity_url_loader_throttle.cc
+++ b/content/common/webid/identity_url_loader_throttle.cc
@@ -97,11 +97,12 @@
   set_idp_status_cb_ = base::BindRepeating(
       [](scoped_refptr<base::SequencedTaskRunner> task_runner,
          SetIdpStatusCallback original_cb,
+         network::mojom::RequestDestination destination,
          const std::optional<url::Origin>& initiator,
          const url::Origin& idp_origin, blink::mojom::IdpSigninStatus status) {
-        task_runner->PostTask(FROM_HERE,
-                              base::BindOnce(std::move(original_cb), initiator,
-                                             idp_origin, status));
+        task_runner->PostTask(
+            FROM_HERE, base::BindOnce(std::move(original_cb), destination,
+                                      initiator, idp_origin, status));
       },
       base::SequencedTaskRunner::GetCurrentDefault(),
       std::move(set_idp_status_cb_));
@@ -112,6 +113,7 @@
     bool* defer) {
   request_url_ = request->url;
   request_initiator_ = request->request_initiator;
+  request_destination_ = request->destination;
 }
 
 void IdentityUrlLoaderThrottle::WillProcessResponse(
@@ -187,13 +189,13 @@
     if (*token == kSetLoginHeaderValueLoggedIn) {
       // Mark IDP as logged in
       VLOG(1) << "IDP signed in: " << idp_origin.Serialize();
-      set_idp_status_cb_.Run(request_initiator_, idp_origin,
-                             IdpSigninStatus::kSignedIn);
+      set_idp_status_cb_.Run(request_destination_, request_initiator_,
+                             idp_origin, IdpSigninStatus::kSignedIn);
     } else if (*token == kSetLoginHeaderValueLoggedOut) {
       // Mark IDP as logged out
       VLOG(1) << "IDP signed out: " << idp_origin.Serialize();
-      set_idp_status_cb_.Run(request_initiator_, idp_origin,
-                             IdpSigninStatus::kSignedOut);
+      set_idp_status_cb_.Run(request_destination_, request_initiator_,
+                             idp_origin, IdpSigninStatus::kSignedOut);
     }
   }
 
diff --git a/content/common/webid/identity_url_loader_throttle.h b/content/common/webid/identity_url_loader_throttle.h
index ec0fb93e..1b025bf 100644
--- a/content/common/webid/identity_url_loader_throttle.h
+++ b/content/common/webid/identity_url_loader_throttle.h
@@ -15,6 +15,7 @@
 #include "content/common/content_export.h"
 #include "content/public/common/web_identity.h"
 #include "net/http/structured_headers.h"
+#include "services/network/public/mojom/fetch_api.mojom.h"
 #include "third_party/blink/public/common/loader/url_loader_throttle.h"
 #include "url/gurl.h"
 #include "url/origin.h"
@@ -57,6 +58,8 @@
 
   GURL request_url_;
   std::optional<url::Origin> request_initiator_;
+  network::mojom::RequestDestination request_destination_ =
+      network::mojom::RequestDestination::kEmpty;
   SetIdpStatusCallback set_idp_status_cb_;
   ParseSetLoginHeaderCallback parse_set_login_header_cb_;
   bool is_inside_handler_response_ = false;
diff --git a/content/public/common/web_identity.h b/content/public/common/web_identity.h
index 08bfaa4..2f4d66b 100644
--- a/content/public/common/web_identity.h
+++ b/content/public/common/web_identity.h
@@ -10,6 +10,7 @@
 #include "base/functional/callback.h"
 #include "content/common/content_export.h"
 #include "net/http/structured_headers.h"
+#include "services/network/public/mojom/fetch_api.mojom-forward.h"
 #include "third_party/blink/public/common/loader/url_loader_throttle.h"
 
 namespace url {
@@ -22,11 +23,11 @@
 
 namespace content {
 
-typedef base::RepeatingCallback<void(
-    const std::optional<url::Origin>& initiator,
-    const url::Origin& idp_origin,
-    blink::mojom::IdpSigninStatus status)>
-    SetIdpStatusCallback;
+using SetIdpStatusCallback =
+    base::RepeatingCallback<void(network::mojom::RequestDestination destination,
+                                 const std::optional<url::Origin>& initiator,
+                                 const url::Origin& idp_origin,
+                                 blink::mojom::IdpSigninStatus status)>;
 
 using ParseSetLoginHeaderCallback = base::RepeatingCallback<void(
     const std::string& header_value,
diff --git a/content/shell/renderer/shell_content_renderer_client.cc b/content/shell/renderer/shell_content_renderer_client.cc
index 5281e4d..34e4055 100644
--- a/content/shell/renderer/shell_content_renderer_client.cc
+++ b/content/shell/renderer/shell_content_renderer_client.cc
@@ -212,6 +212,7 @@
               [](const blink::LocalFrameToken& token,
                  const scoped_refptr<base::SequencedTaskRunner>
                      main_thread_task_runner,
+                 network::mojom::RequestDestination destination,
                  const std::optional<url::Origin>& initiator,
                  const url::Origin& idp_origin,
                  blink::mojom::IdpSigninStatus status) {
diff --git a/headless/lib/renderer/headless_content_renderer_client.cc b/headless/lib/renderer/headless_content_renderer_client.cc
index e37d8724..df349d3 100644
--- a/headless/lib/renderer/headless_content_renderer_client.cc
+++ b/headless/lib/renderer/headless_content_renderer_client.cc
@@ -56,6 +56,7 @@
               [](const blink::LocalFrameToken& token,
                  const scoped_refptr<base::SequencedTaskRunner>
                      main_thread_task_runner,
+                 network::mojom::RequestDestination destination,
                  const std::optional<url::Origin>& initiator,
                  const url::Origin& idp_origin,
                  blink::mojom::IdpSigninStatus status) {
diff --git a/content/common/webid/identity_url_loader_throttle_unittest.cc b/content/common/webid/identity_url_loader_throttle_unittest.cc
index 23da808..b647459 100644
--- a/content/common/webid/identity_url_loader_throttle_unittest.cc
+++ b/content/common/webid/identity_url_loader_throttle_unittest.cc
@@ -44,16 +44,20 @@
     return GetSetLoginHeaderInProcessParser();
   }
 
-  void SetIdpStatus(const std::optional<url::Origin>& initiator,
+  void SetIdpStatus(network::mojom::RequestDestination destination,
+                    const std::optional<url::Origin>& initiator,
                     const url::Origin& idp_origin,
                     IdpSigninStatus status) {
     ++cb_num_calls_;
+    cb_destination_ = destination;
     cb_initiator_ = initiator;
     cb_idp_origin_ = idp_origin;
     cb_signin_status_ = status;
   }
 
   int cb_num_calls_ = 0;
+  network::mojom::RequestDestination cb_destination_ =
+      network::mojom::RequestDestination::kEmpty;
   std::optional<url::Origin> cb_initiator_;
   url::Origin cb_idp_origin_;
   IdpSigninStatus cb_signin_status_ = IdpSigninStatus::kSignedOut;
@@ -76,6 +80,7 @@
   network::ResourceRequest request;
   request.url = GURL("https://accounts.idp.example/");
   request.request_initiator = url::Origin::Create(GURL("https://rp.example/"));
+  request.destination = network::mojom::RequestDestination::kDocument;
   bool defer = false;
 
   throttle->WillStartRequest(&request, &defer);
@@ -96,6 +101,7 @@
   EXPECT_EQ(url::Origin::Create(GURL("https://accounts.idp.example/")),
             cb_idp_origin_);
   EXPECT_EQ(request.request_initiator, cb_initiator_);
+  EXPECT_EQ(request.destination, cb_destination_);
   EXPECT_EQ(0, delegate.resume_called_);
 }
 
@@ -129,6 +135,32 @@
   EXPECT_EQ(0, delegate.resume_called_);
 }
 
+TEST_F(IdentityUrlLoaderThrottleTest, NonDefaultDestination) {
+  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/");
+  request.destination = network::mojom::RequestDestination::kScript;
+  bool defer = false;
+
+  throttle->WillStartRequest(&request, &defer);
+  EXPECT_FALSE(defer);
+
+  network::mojom::URLResponseHead response_head;
+  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(1, cb_num_calls_);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript, cb_destination_);
+}
+
 TEST_F(IdentityUrlLoaderThrottleTest, InvalidHeader) {
   TestDelegate delegate;
   std::unique_ptr<blink::URLLoaderThrottle> throttle =
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/common/webid/identity_url_loader_throttle_unittest.cc b/content/common/webid/identity_url_loader_throttle_unittest.cc
index 23da808..b647459 100644
--- a/content/common/webid/identity_url_loader_throttle_unittest.cc
+++ b/content/common/webid/identity_url_loader_throttle_unittest.cc
@@ -44,16 +44,20 @@
     return GetSetLoginHeaderInProcessParser();
   }
 
-  void SetIdpStatus(const std::optional<url::Origin>& initiator,
+  void SetIdpStatus(network::mojom::RequestDestination destination,
+                    const std::optional<url::Origin>& initiator,
                     const url::Origin& idp_origin,
                     IdpSigninStatus status) {
     ++cb_num_calls_;
+    cb_destination_ = destination;
     cb_initiator_ = initiator;
     cb_idp_origin_ = idp_origin;
     cb_signin_status_ = status;
   }
 
   int cb_num_calls_ = 0;
+  network::mojom::RequestDestination cb_destination_ =
+      network::mojom::RequestDestination::kEmpty;
   std::optional<url::Origin> cb_initiator_;
   url::Origin cb_idp_origin_;
   IdpSigninStatus cb_signin_status_ = IdpSigninStatus::kSignedOut;
@@ -76,6 +80,7 @@
   network::ResourceRequest request;
   request.url = GURL("https://accounts.idp.example/");
   request.request_initiator = url::Origin::Create(GURL("https://rp.example/"));
+  request.destination = network::mojom::RequestDestination::kDocument;
   bool defer = false;
 
   throttle->WillStartRequest(&request, &defer);
@@ -96,6 +101,7 @@
   EXPECT_EQ(url::Origin::Create(GURL("https://accounts.idp.example/")),
             cb_idp_origin_);
   EXPECT_EQ(request.request_initiator, cb_initiator_);
+  EXPECT_EQ(request.destination, cb_destination_);
   EXPECT_EQ(0, delegate.resume_called_);
 }
 
@@ -129,6 +135,32 @@
   EXPECT_EQ(0, delegate.resume_called_);
 }
 
+TEST_F(IdentityUrlLoaderThrottleTest, NonDefaultDestination) {
+  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/");
+  request.destination = network::mojom::RequestDestination::kScript;
+  bool defer = false;
+
+  throttle->WillStartRequest(&request, &defer);
+  EXPECT_FALSE(defer);
+
+  network::mojom::URLResponseHead response_head;
+  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(1, cb_num_calls_);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript, cb_destination_);
+}
+
 TEST_F(IdentityUrlLoaderThrottleTest, InvalidHeader) {
   TestDelegate delegate;
   std::unique_ptr<blink::URLLoaderThrottle> throttle =
Loading diff…

Original Bug Report

reported by vm...@google.com

FedCM Set-Login same-site bypass via Signed Exchange cert fetches

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic flaw in how Signed Exchange (SXG) certificate fetches are handled allows a cross-origin site to bypass FedCM Set-Login same-site enforcement. Because the cert fetch reuses the URLLoaderThrottles generated for the top-level navigation, the identity throttle incorrectly assumes a kDocument destination. This allows a tracker to maliciously mark itself as signed-in globally without a same-site visit.

Affected files:

  • content/browser/webid/webid_utils.cc
  • content/browser/loader/url_loader_throttles.cc
  • content/browser/web_package/signed_exchange_cert_fetcher_factory.cc
  • content/common/webid/identity_url_loader_throttle.cc
  • chrome/browser/webid/federated_identity_permission_context.cc

Estimated timestamp from git blame: 2026-04-22

Root Cause

The FedCM Set-Login header same-site enforcement in webid::SetIdpSigninStatus contains a logic flaw that allows a bypass during Signed Exchange (SXG) certificate fetches.

When processing a Set-Login header, webid::SetIdpSigninStatus (in content/browser/webid/webid_utils.cc) performs two security checks to prevent trackers from arbitrarily setting a user’s login status:

  1. If the request destination is not kDocument, it ensures the request initiator is same-site with the Identity Provider (IDP) origin.
  2. It verifies that the IDP origin is same-site with all frame ancestors, starting from the FrameTreeNode’s parent.

While this logic works for standard requests, it breaks during Signed Exchange navigations due to throttle reuse. When a navigation is initiated, NavigationURLLoaderImpl creates throttles based on the main resource_request(), binding the IdentityUrlLoaderThrottle callback with request.destination = network::mojom::RequestDestination::kDocument.

If the response is a Signed Exchange, a SignedExchangeLoader is created. This loader initializes a SignedExchangeCertFetcherFactory, passing it the URLLoaderThrottlesGetter callback from the original navigation request. When the SignedExchangeHandler parses the SXG signature and needs to fetch the cert-url, it invokes this factory.

The factory executes the throttle getter, generating a new IdentityUrlLoaderThrottle, but because it relies on the original navigation request’s state, the throttle’s callback is still bound with destination = kDocument.

If the cert-url endpoint responds with a Set-Login header, IdentityUrlLoaderThrottle parses it and calls webid::SetIdpSigninStatus. Because the destination was incorrectly bound as kDocument, the cross-origin initiator check is skipped entirely. Furthermore, because it is a top-level navigation, the ancestor check passes (the parent frame is null). As a result, the cert-url origin is marked as signed-in profile-wide without ever having been visited in a same-site context.

Potential Exploit Steps

Note: These are potential steps based on code analysis; we do not currently have a working proof of concept.

  1. An attacker configures a cross-origin tracking domain to serve a Set-Login: logged-in header from a specific endpoint.
  2. The attacker hosts a malicious Signed Exchange (SXG) file on an attacker-controlled server. They configure the SXG’s signature header to use the cross-origin tracker’s endpoint as the cert-url.
  3. The attacker tricks a victim into navigating to the hosted SXG file.
  4. The browser begins a top-level navigation, discovers the SXG, and initiates a background fetch to the cert-url to verify the signature.
  5. The tracker’s endpoint responds with the Set-Login header.
  6. The browser parses the header, bypasses the same-site FedCM checks due to the inherited kDocument destination state, and marks the tracker as signed-in globally in the user’s profile.

Suggested Fix

The SignedExchangeCertFetcher should not blindly inherit the kDocument destination from the top-level navigation when generating URLLoaderThrottles. The throttles generated for the certificate fetch should reflect its nature as a subresource fetch, or the IdentityUrlLoaderThrottle should specifically check if it is being executed within the context of a Signed Exchange certificate fetch and enforce same-site constraints accordingly.

Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

View on issue tracker