Medium watch CVSS 6.5 chrome Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Enterprise
DescriptionInformation leak in Enterprise
ComponentEnterprise
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker520389619
Fix commit5ecc573cd0c9 (chromium/src) +386/-13
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-07-29 (public git)
Disclosed2026-09-08
Patch gap41 days — fix public in source before the advisory

Background

`CloudApProviderWin`
the Windows enterprise platform-auth provider that fetches proof-of-possession auth cookies/headers for a target GURL via a COM call (GetAuthData) and returns them to PlatformAuthProviderManager to attach to network requests.
`GetData(url, callback)`
the provider entry point that requests auth headers for a specific url and invokes callback with the resulting net::HttpRequestHeaders.
`on_get_data_callback_list_` (legacy)
a single base::OnceCallbackList of all pending GetData callbacks, run together by OnGetDataCallback() when a fetch completes, without any association to the URL that fetch was for.
URL-keyed request queue (new)
request_queues_, a std::map<GURL, std::vector<GetDataCallback>> that groups pending callbacks by their exact target GURL so a fetch’s result is delivered only to matching requests.

Root Cause Analysis

In the legacy path, every GetData() call appended its callback to the single global on_get_data_callback_list_ and posted a fetch for its own url; when any one fetch completed, OnGetDataCallback() ran the provided net::HttpRequestHeaders against all stored callbacks. Because the callback list was not keyed by URL, concurrent requests for different URLs were all satisfied by whichever fetch finished, so auth headers computed for one target GURL could be delivered to a callback that had requested a different GURL. Those headers are proof-of-possession auth cookies scoped to a specific origin, so cross-delivering them attaches one origin’s credentials to another origin’s request — an information leak.

The fix (gated by kCloudApAuthDataQueueing, on by default) routes requests into request_queues_ keyed by exact GURL, fetches at most once per unique URL via StartFetch(), and in OnFetchCompleted() distributes the headers strictly and only to the callbacks queued under that same URL. It also caps total_enqueued_requests_ at kMaxQueueSize (100), failing overflow requests with empty headers to bound memory use.

Key insight
The core mistake was fanning a single fetch’s auth headers out to a global, non-URL-keyed callback list, so credentials for one URL could be handed to a request for another; the fix keys pending callbacks by their exact target GURL and delivers each completed fetch’s headers only to that URL’s queue.

Attack Path

  1. Enterprise SSO context On a managed Windows device with cloud/Entra platform SSO, PlatformAuthProviderManager calls CloudApProviderWin::GetData() to obtain proof-of-possession auth headers for outgoing requests to relevant URLs.
  2. Concurrent differing-URL requests Two or more GetData() calls for distinct target URLs are in flight at once, each appended to the shared on_get_data_callback_list_.
  3. First fetch broadcasts The first COM fetch to complete invokes OnGetDataCallback(), which runs its net::HttpRequestHeaders against every queued callback, including callbacks that requested a different URL.
  4. Cross-URL credential delivery A request for URL B receives auth headers fetched for URL A, so origin A’s proof-of-possession credentials are returned for (inferred: and attached to) URL B’s request, disclosing them cross-origin.

Impact Assessment

The flaw lets auth headers (proof-of-possession cookies) intended for one target URL be delivered to a pending request for a different URL within the browser process’s enterprise platform-auth path, leaking credentials across origins; the precondition is a managed device using CloudApProviderWin platform SSO with concurrent auth-data requests for differing URLs. The advisory classifies this as Information leak in Enterprise and does not report in-the-wild exploitation. The patch also adds a 100-request global queue cap addressing memory-exhaustion/DoS, but any impact beyond credential disclosure and the noted DoS is not demonstrated (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc
modified
GURL
chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h
modified
CloudApProviderWinTest
chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
modified
TEST_F
chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
modified
for
chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
modified

Files Changed

  • chrome/browser/enterprise/platform_auth/platform_auth_features.cc
  • tools/metrics/histograms/metadata/enterprise/histograms.xml
  • chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc
  • chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
  • chrome/browser/enterprise/platform_auth/platform_auth_provider_manager_browsertest.cc
  • chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h
  • chrome/browser/enterprise/platform_auth/platform_auth_features.h

Audit Directions

  • Result fan-out without a request key
    Audit async providers that store pending callbacks in a shared list and run them all on a single completion; verify each completion is matched to the request that produced it (by URL, origin, or request ID).
  • Origin-scoped secrets crossing requests
    Wherever credentials, tokens, or cookies are computed per-origin/per-URL and delivered via callbacks, confirm the delivery target’s origin equals the origin the secret was minted for.
  • Concurrent-request merging correctness
    Review request-coalescing/merging logic for the assumption that in-flight requests are identical; ensure merging keys on the full identity (exact GURL) rather than delivering one fetch’s result to unrelated waiters.
From 5ecc573cd0c95e8d5513a14836adb41d7c9f31eb Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <toyoshim@chromium.org>
Date: Tue, 28 Jul 2026 22:40:25 -0700
Subject: [PATCH] Improve CloudApProviderWin to manage requests in a queue

This refactoring implements a URL-keyed request merging mechanism:
- Grouping concurrent auth requests by their precise target GURL.
- Restricting systemic IDP/COM token fetching operations to run exactly once
  per unique URL.
- Executing an atomic token broadcast that distributes copied auth headers
  strictly and exclusively to matching requests.
- Enforcing a strict browser-wide ceiling (capped at 100 pending requests)
  to protect against systemic memory-exhaustion/DoS attacks.

Bug: 520389619
Change-Id: I76725a71eb08fb84316bb68d311f6851081d3760
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7924786
Commit-Queue: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Igor Ruvinov <igorruvinov@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1670002}
---
diff --git a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc
index ebaea83f..4d9b090 100644
--- a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc
+++ b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc
@@ -437,16 +437,43 @@
 void CloudApProviderWin::GetData(
     const GURL& url,
     PlatformAuthProviderManager::GetDataCallback callback) {
-  get_data_subscriptions_.push_back(
-      on_get_data_callback_list_.Add(std::move(callback)));
-  if (!base::ThreadPool::CreateCOMSTATaskRunner(
-           {base::TaskPriority::USER_BLOCKING,
-            base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN, base::MayBlock()})
-           ->PostTaskAndReplyWithResult(
-               FROM_HERE, base::BindOnce(&GetAuthData, url),
-               base::BindOnce(&CloudApProviderWin::OnGetDataCallback,
-                              base::Unretained(this)))) {
-    OnGetDataCallback(net::HttpRequestHeaders());
+  if (!base::FeatureList::IsEnabled(
+          enterprise_auth::kCloudApAuthDataQueueing)) {
+    get_data_subscriptions_.push_back(
+        on_get_data_callback_list_.Add(std::move(callback)));
+    if (!base::ThreadPool::CreateCOMSTATaskRunner(
+             {base::TaskPriority::USER_BLOCKING,
+              base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN,
+              base::MayBlock()})
+             ->PostTaskAndReplyWithResult(
+                 FROM_HERE, base::BindOnce(&GetAuthData, url),
+                 base::BindOnce(&CloudApProviderWin::OnGetDataCallback,
+                                base::Unretained(this)))) {
+      OnGetDataCallback(net::HttpRequestHeaders());
+    }
+    return;
+  }
+
+  if (total_enqueued_requests_ >= kMaxQueueSize) {
+    VLOG_POLICY(1, EXTENSIBLE_SSO)
+        << "[CloudAPAuthEnabled] Enqueued requests limit (" << kMaxQueueSize
+        << ") exceeded. Failing request.";
+    base::UmaHistogramBoolean(
+        "Enterprise.PlatformAuth.GetAuthData.QueueOverflow", true);
+    // Global queue limit exceeded, fail the request asynchronously.
+    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE,
+        base::BindOnce(std::move(callback), net::HttpRequestHeaders()));
+    return;
+  }
+
+  total_enqueued_requests_++;
+  auto& callbacks = request_queues_[url];
+  bool is_first_request = callbacks.empty();
+  callbacks.push_back(std::move(callback));
+
+  if (is_first_request) {
+    StartFetch(url);
   }
 }
 
@@ -472,4 +499,38 @@
   ParseCookieInfo(cookie_info, cookie_info_count, auth_headers);
 }
 
+void CloudApProviderWin::StartFetch(const GURL& url) {
+  if (!base::ThreadPool::CreateCOMSTATaskRunner(
+           {base::TaskPriority::USER_BLOCKING,
+            base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN, base::MayBlock()})
+           ->PostTaskAndReplyWithResult(
+               FROM_HERE, base::BindOnce(&GetAuthData, url),
+               base::BindOnce(&CloudApProviderWin::OnFetchCompleted,
+                              weak_factory_.GetWeakPtr(), url))) {
+    OnFetchCompleted(url, net::HttpRequestHeaders());
+  }
+}
+
+void CloudApProviderWin::OnFetchCompleted(
+    const GURL& url,
+    net::HttpRequestHeaders auth_headers) {
+  auto it = request_queues_.find(url);
+  CHECK(it != request_queues_.end());
+  CHECK(!it->second.empty());
+
+  std::vector<PlatformAuthProviderManager::GetDataCallback> callbacks =
+      std::move(it->second);
+  request_queues_.erase(it);
+
+  CHECK_GE(total_enqueued_requests_, callbacks.size());
+  total_enqueued_requests_ -= callbacks.size();
+
+  for (size_t i = 0; i < callbacks.size(); ++i) {
+    auto callback = std::move(callbacks[i]);
+    net::HttpRequestHeaders headers =
+        (i + 1 == callbacks.size()) ? std::move(auth_headers) : auth_headers;
+    std::move(callback).Run(std::move(headers));
+  }
+}
+
 }  // namespace enterprise_auth
diff --git a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h
index dff86da..bcc9696f 100644
--- a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h
+++ b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h
@@ -5,11 +5,13 @@
 #ifndef CHROME_BROWSER_ENTERPRISE_PLATFORM_AUTH_CLOUD_AP_PROVIDER_WIN_H_
 #define CHROME_BROWSER_ENTERPRISE_PLATFORM_AUTH_CLOUD_AP_PROVIDER_WIN_H_
 
+#include <map>
 #include <optional>
 #include <vector>
 
 #include "base/callback_list.h"
 #include "base/gtest_prod_util.h"
+#include "base/memory/weak_ptr.h"
 #include "chrome/browser/enterprise/platform_auth/platform_auth_provider.h"
 
 class GURL;
@@ -52,9 +54,13 @@
   FRIEND_TEST_ALL_PREFIXES(CloudApProviderWinTest,
                            ParseCookieInfo_HeaderFeatureEnabled);
 
-  // Runs the stored callbacks using the provided auth headers.
+  // Runs the stored callbacks using the provided auth headers (legacy
+  // non-queuing implementation).
   void OnGetDataCallback(net::HttpRequestHeaders auth_headers);
 
+  // Maximum total number of pending data requests allowed across all URLs.
+  static constexpr size_t kMaxQueueSize = 100;
+
   // Overrides support detection with `level` if it has a value, or resets the
   // override if not.
   static void SetSupportLevelForTesting(std::optional<SupportLevel> level);
@@ -64,14 +70,30 @@
                                  const DWORD cookie_info_count,
                                  net::HttpRequestHeaders& auth_headers);
 
-  // List of callbacks to run when auth data is received.
+  // List of callbacks to run when auth data is received (legacy non-queuing
+  // implementation).
   using GetDataCallbackList =
       base::OnceCallbackList<void(net::HttpRequestHeaders)>;
   GetDataCallbackList on_get_data_callback_list_;
 
   // Subscriptions for auth data requests. Guarantees that the corresponding
-  // callbacks are run on destruction.
+  // callbacks are run on destruction (legacy non-queuing implementation).
   std::vector<base::CallbackListSubscription> get_data_subscriptions_;
+
+  // Starts a background task to fetch auth data for `url`.
+  void StartFetch(const GURL& url);
+
+  // Handles completion of a background fetch for `url`.
+  void OnFetchCompleted(const GURL& url, net::HttpRequestHeaders auth_headers);
+
+  std::map<GURL, std::vector<PlatformAuthProviderManager::GetDataCallback>>
+      request_queues_;
+
+  // Total number of pending requests stored across all vectors in
+  // `request_queues_`.
+  size_t total_enqueued_requests_ = 0;
+
+  base::WeakPtrFactory<CloudApProviderWin> weak_factory_{this};
 };
 
 }  // namespace enterprise_auth
diff --git a/chrome/browser/enterprise/platform_auth/platform_auth_features.cc b/chrome/browser/enterprise/platform_auth/platform_auth_features.cc
index 239ea5c..6084dd98 100644
--- a/chrome/browser/enterprise/platform_auth/platform_auth_features.cc
+++ b/chrome/browser/enterprise/platform_auth/platform_auth_features.cc
@@ -12,4 +12,6 @@
 
 BASE_FEATURE(kAndroidEntraSSO, base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kCloudApAuthDataQueueing, base::FEATURE_ENABLED_BY_DEFAULT);
+
 }  // namespace enterprise_auth
diff --git a/chrome/browser/enterprise/platform_auth/platform_auth_features.h b/chrome/browser/enterprise/platform_auth/platform_auth_features.h
index 28257580..7fc3d52 100644
--- a/chrome/browser/enterprise/platform_auth/platform_auth_features.h
+++ b/chrome/browser/enterprise/platform_auth/platform_auth_features.h
@@ -13,6 +13,10 @@
 
 BASE_DECLARE_FEATURE(kAndroidEntraSSO);
 
+// Kill switch for the new data queuing and concurrent request handling in
+// CloudApProviderWin. Enabled by default. See http://crbug.com/520389619.
+BASE_DECLARE_FEATURE(kCloudApAuthDataQueueing);
+
 }  // namespace enterprise_auth
 
 #endif  // CHROME_BROWSER_ENTERPRISE_PLATFORM_AUTH_PLATFORM_AUTH_FEATURES_H_
diff --git a/tools/metrics/histograms/metadata/enterprise/histograms.xml b/tools/metrics/histograms/metadata/enterprise/histograms.xml
index f200c8c2..a14fe15 100644
--- a/tools/metrics/histograms/metadata/enterprise/histograms.xml
+++ b/tools/metrics/histograms/metadata/enterprise/histograms.xml
@@ -3599,6 +3599,17 @@
   </summary>
 </histogram>
 
+<histogram name="Enterprise.PlatformAuth.GetAuthData.QueueOverflow"
+    enum="BooleanOccurred" expires_after="2027-07-28">
+  <owner>igorruvinov@chromium.org</owner>
+  <owner>chromium-enterprise@chromium.org</owner>
+  <summary>
+    Recorded when an incoming request for proof-of-possession cookies from the
+    platform is dropped because the maximum number of enqueued requests has been
+    reached.
+  </summary>
+</histogram>
+
 <histogram name="Enterprise.PlatformAuth.GetAuthData.{Result}Time" units="ms"
     expires_after="2026-10-28">
   <owner>igorruvinov@chromium.org</owner>
diff --git a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
index f885dfc..bc3da91 100644
--- a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
+++ b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
@@ -9,9 +9,12 @@
 #include <memory>
 #include <vector>
 
+#include "base/barrier_closure.h"
 #include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
@@ -42,6 +45,21 @@
 
 class CloudApProviderWinTest : public ::testing::Test {
  protected:
+  static constexpr size_t kMaxQueueSize = CloudApProviderWin::kMaxQueueSize;
+
+  static size_t GetTotalFetches(const base::HistogramTester& tester) {
+    size_t total = 0;
+    for (const auto& bucket : tester.GetAllSamples(
+             "Enterprise.PlatformAuth.GetAuthData.SuccessTime")) {
+      total += bucket.count;
+    }
+    for (const auto& bucket : tester.GetAllSamples(
+             "Enterprise.PlatformAuth.GetAuthData.FailureTime")) {
+      total += bucket.count;
+    }
+    return total;
+  }
+
   ~CloudApProviderWinTest() override {
     // Clear an override of the join type made by any test.
     CloudApProviderWin::SetSupportLevelForTesting(std::nullopt);
@@ -198,4 +216,155 @@
             base::WideToASCII(L"data"));
 }
 
+TEST_F(CloudApProviderWinTest, GetDataQueueAndOverflow) {
+  CloudApProviderWin provider;
+  GURL url("https://test.com");
+  base::HistogramTester histogram_tester;
+
+  int success_count = 0;
+  int overflow_count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(kMaxQueueSize + 1, run_loop.QuitClosure());
+
+  // Queue up to the max size (100 items).
+  for (size_t i = 0; i < kMaxQueueSize; ++i) {
+    provider.GetData(url,
+                     base::BindOnce(
+                         [](int* success_count, base::RepeatingClosure barrier,
+                            net::HttpRequestHeaders headers) {
+                           (*success_count)++;
+                           barrier.Run();
+                         },
+                         &success_count, barrier_closure));
+  }
+
+  // The 101st request should overflow.
+  provider.GetData(url,
+                   base::BindOnce(
+                       [](int* overflow_count, base::RepeatingClosure barrier,
+                          net::HttpRequestHeaders headers) {
+                         // Overflow requests are completed with empty headers.
+                         EXPECT_TRUE(headers.IsEmpty());
+                         (*overflow_count)++;
+                         barrier.Run();
+                       },
+                       &overflow_count, barrier_closure));
+
+  run_loop.Run();
+
+  EXPECT_EQ(success_count, static_cast<int>(kMaxQueueSize));
+  EXPECT_EQ(overflow_count, 1);
+
+  // Check that the queue overflow histogram recorded 1 sample.
+  histogram_tester.ExpectBucketCount(
+      "Enterprise.PlatformAuth.GetAuthData.QueueOverflow", true, 1);
+
+  // Exactly 1 background fetch should have occurred for the 100 merged
+  // requests.
+  EXPECT_EQ(GetTotalFetches(histogram_tester), 1u);
+}
+
+TEST_F(CloudApProviderWinTest, GetDataSameUrl) {
+  CloudApProviderWin provider;
+  GURL url("https://test.com");
+  base::HistogramTester histogram_tester;
+
+  int count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(5, run_loop.QuitClosure());
+
+  for (int i = 0; i < 5; ++i) {
+    provider.GetData(url, base::BindOnce(
+                              [](int* count, base::RepeatingClosure barrier,
+                                 net::HttpRequestHeaders) {
+                                (*count)++;
+                                barrier.Run();
+                              },
+                              &count, barrier_closure));
+  }
+
+  run_loop.Run();
+
+  EXPECT_EQ(count, 5);
+  // Only 1 background fetch should have occurred for the 5 merged same-URL
+  // requests.
+  EXPECT_EQ(GetTotalFetches(histogram_tester), 1u);
+}
+
+TEST_F(CloudApProviderWinTest, GetDataMultipleUrls) {
+  CloudApProviderWin provider;
+  GURL url1("https://test1.com");
+  GURL url2("https://test2.com");
+
+  int count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(2, run_loop.QuitClosure());
+
+  auto cb = [](int* count, base::RepeatingClosure barrier,
+               net::HttpRequestHeaders) {
+    (*count)++;
+    barrier.Run();
+  };
+
+  provider.GetData(url1, base::BindOnce(cb, &count, barrier_closure));
+  provider.GetData(url2, base::BindOnce(cb, &count, barrier_closure));
+
+  run_loop.Run();
+
+  EXPECT_EQ(count, 2);
+}
+
+TEST_F(CloudApProviderWinTest, GetDataGlobalOverflow) {
+  CloudApProviderWin provider;
+  base::HistogramTester histogram_tester;
+
+  int success_count = 0;
+  int overflow_count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(kMaxQueueSize + 1, run_loop.QuitClosure());
+
+  // Queue up to the global max size across distinct URLs.
+  for (size_t i = 0; i < kMaxQueueSize; ++i) {
+    GURL url(base::JoinString({"https://test", base::NumberToString(i), ".com"},
+                              ""));
+    provider.GetData(url,
+                     base::BindOnce(
+                         [](int* success_count, base::RepeatingClosure barrier,
+                            net::HttpRequestHeaders headers) {
+                           (*success_count)++;
+                           barrier.Run();
+                         },
+                         &success_count, barrier_closure));
+  }
+
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
index f885dfc..bc3da91 100644
--- a/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
+++ b/chrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
@@ -9,9 +9,12 @@
 #include <memory>
 #include <vector>
 
+#include "base/barrier_closure.h"
 #include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
@@ -42,6 +45,21 @@
 
 class CloudApProviderWinTest : public ::testing::Test {
  protected:
+  static constexpr size_t kMaxQueueSize = CloudApProviderWin::kMaxQueueSize;
+
+  static size_t GetTotalFetches(const base::HistogramTester& tester) {
+    size_t total = 0;
+    for (const auto& bucket : tester.GetAllSamples(
+             "Enterprise.PlatformAuth.GetAuthData.SuccessTime")) {
+      total += bucket.count;
+    }
+    for (const auto& bucket : tester.GetAllSamples(
+             "Enterprise.PlatformAuth.GetAuthData.FailureTime")) {
+      total += bucket.count;
+    }
+    return total;
+  }
+
   ~CloudApProviderWinTest() override {
     // Clear an override of the join type made by any test.
     CloudApProviderWin::SetSupportLevelForTesting(std::nullopt);
@@ -198,4 +216,155 @@
             base::WideToASCII(L"data"));
 }
 
+TEST_F(CloudApProviderWinTest, GetDataQueueAndOverflow) {
+  CloudApProviderWin provider;
+  GURL url("https://test.com");
+  base::HistogramTester histogram_tester;
+
+  int success_count = 0;
+  int overflow_count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(kMaxQueueSize + 1, run_loop.QuitClosure());
+
+  // Queue up to the max size (100 items).
+  for (size_t i = 0; i < kMaxQueueSize; ++i) {
+    provider.GetData(url,
+                     base::BindOnce(
+                         [](int* success_count, base::RepeatingClosure barrier,
+                            net::HttpRequestHeaders headers) {
+                           (*success_count)++;
+                           barrier.Run();
+                         },
+                         &success_count, barrier_closure));
+  }
+
+  // The 101st request should overflow.
+  provider.GetData(url,
+                   base::BindOnce(
+                       [](int* overflow_count, base::RepeatingClosure barrier,
+                          net::HttpRequestHeaders headers) {
+                         // Overflow requests are completed with empty headers.
+                         EXPECT_TRUE(headers.IsEmpty());
+                         (*overflow_count)++;
+                         barrier.Run();
+                       },
+                       &overflow_count, barrier_closure));
+
+  run_loop.Run();
+
+  EXPECT_EQ(success_count, static_cast<int>(kMaxQueueSize));
+  EXPECT_EQ(overflow_count, 1);
+
+  // Check that the queue overflow histogram recorded 1 sample.
+  histogram_tester.ExpectBucketCount(
+      "Enterprise.PlatformAuth.GetAuthData.QueueOverflow", true, 1);
+
+  // Exactly 1 background fetch should have occurred for the 100 merged
+  // requests.
+  EXPECT_EQ(GetTotalFetches(histogram_tester), 1u);
+}
+
+TEST_F(CloudApProviderWinTest, GetDataSameUrl) {
+  CloudApProviderWin provider;
+  GURL url("https://test.com");
+  base::HistogramTester histogram_tester;
+
+  int count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(5, run_loop.QuitClosure());
+
+  for (int i = 0; i < 5; ++i) {
+    provider.GetData(url, base::BindOnce(
+                              [](int* count, base::RepeatingClosure barrier,
+                                 net::HttpRequestHeaders) {
+                                (*count)++;
+                                barrier.Run();
+                              },
+                              &count, barrier_closure));
+  }
+
+  run_loop.Run();
+
+  EXPECT_EQ(count, 5);
+  // Only 1 background fetch should have occurred for the 5 merged same-URL
+  // requests.
+  EXPECT_EQ(GetTotalFetches(histogram_tester), 1u);
+}
+
+TEST_F(CloudApProviderWinTest, GetDataMultipleUrls) {
+  CloudApProviderWin provider;
+  GURL url1("https://test1.com");
+  GURL url2("https://test2.com");
+
+  int count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(2, run_loop.QuitClosure());
+
+  auto cb = [](int* count, base::RepeatingClosure barrier,
+               net::HttpRequestHeaders) {
+    (*count)++;
+    barrier.Run();
+  };
+
+  provider.GetData(url1, base::BindOnce(cb, &count, barrier_closure));
+  provider.GetData(url2, base::BindOnce(cb, &count, barrier_closure));
+
+  run_loop.Run();
+
+  EXPECT_EQ(count, 2);
+}
+
+TEST_F(CloudApProviderWinTest, GetDataGlobalOverflow) {
+  CloudApProviderWin provider;
+  base::HistogramTester histogram_tester;
+
+  int success_count = 0;
+  int overflow_count = 0;
+  base::RunLoop run_loop;
+  base::RepeatingClosure barrier_closure =
+      base::BarrierClosure(kMaxQueueSize + 1, run_loop.QuitClosure());
+
+  // Queue up to the global max size across distinct URLs.
+  for (size_t i = 0; i < kMaxQueueSize; ++i) {
+    GURL url(base::JoinString({"https://test", base::NumberToString(i), ".com"},
+                              ""));
+    provider.GetData(url,
+                     base::BindOnce(
+                         [](int* success_count, base::RepeatingClosure barrier,
+                            net::HttpRequestHeaders headers) {
+                           (*success_count)++;
+                           barrier.Run();
+                         },
+                         &success_count, barrier_closure));
+  }
+
+  // The next request (even to a new URL) should overflow due to the global
+  // limit.
+  GURL overflow_url("https://overflow.com");
+  provider.GetData(overflow_url,
+                   base::BindOnce(
+                       [](int* overflow_count, base::RepeatingClosure barrier,
+                          net::HttpRequestHeaders headers) {
+                         EXPECT_TRUE(headers.IsEmpty());
+                         (*overflow_count)++;
+                         barrier.Run();
+                       },
+                       &overflow_count, barrier_closure));
+
+  run_loop.Run();
+
+  EXPECT_EQ(success_count, static_cast<int>(kMaxQueueSize));
+  EXPECT_EQ(overflow_count, 1);
+
+  // Check that the queue overflow histogram recorded 1 sample.
+  histogram_tester.ExpectBucketCount(
+      "Enterprise.PlatformAuth.GetAuthData.QueueOverflow", true, 1);
+
+  // 100 distinct URLs should trigger 100 distinct background fetches.
+  EXPECT_EQ(GetTotalFetches(histogram_tester), kMaxQueueSize);
+}
+
 }  // namespace enterprise_auth
diff --git a/chrome/browser/enterprise/platform_auth/platform_auth_provider_manager_browsertest.cc b/chrome/browser/enterprise/platform_auth/platform_auth_provider_manager_browsertest.cc
index 7ecd5b09..f927164d 100644
--- a/chrome/browser/enterprise/platform_auth/platform_auth_provider_manager_browsertest.cc
+++ b/chrome/browser/enterprise/platform_auth/platform_auth_provider_manager_browsertest.cc
@@ -4,8 +4,15 @@
 
 #include "chrome/browser/enterprise/platform_auth/platform_auth_provider_manager.h"
 
+#include "base/synchronization/lock.h"
+#include "base/test/bind.h"
+#include "build/build_config.h"
 #include "chrome/browser/enterprise/platform_auth/mock_platform_auth_provider.h"
 #include "chrome/browser/enterprise/platform_auth/scoped_set_provider_for_testing.h"
+#if !BUILDFLAG(IS_ANDROID)
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_tabstrip.h"
+#endif
 #include "chrome/test/base/chrome_test_utils.h"
 #include "chrome/test/base/platform_browser_test.h"
 #include "content/public/test/browser_test.h"
@@ -127,4 +134,101 @@
   EXPECT_CALL(*unsafe_mock_provider, Die());
 }
 
+#if !BUILDFLAG(IS_ANDROID)
+IN_PROC_BROWSER_TEST_F(PlatformAuthManagerBrowserTest, ConcurrentNavigations) {
+  base::Lock lock;
+  std::map<GURL, std::string> received_cookies;
+
+  embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting(
+      [&](const net::test_server::HttpRequest& request)
+          -> std::unique_ptr<net::test_server::HttpResponse> {
+        // Filter out extraneous requests (e.g., /favicon.ico) automatically
+        // triggered by the browser after page load. Handling them here would
+        // cause lock contention and deadlock (DCHECK failure in base::Lock)
+        // against the main thread during the verification phase.
+        if (request.GetURL().path() != "/title1.html" &&
+            request.GetURL().path() != "/title2.html") {
+          return nullptr;
+        }
+        base::AutoLock auto_lock(lock);
+        if (request.headers.find("cookie") != request.headers.end()) {
+          received_cookies[request.GetURL()] = request.headers.at("cookie");
+        }
+        return nullptr;
+      }));
+
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  auto mock_provider =
+      std::make_unique<::testing::StrictMock<MockPlatformAuthProvider>>();
+  EXPECT_CALL(*mock_provider, SupportsOriginFiltering())
+      .WillRepeatedly(::testing::Return(false));
+
+  base::RunLoop get_data_run_loop;
+  std::map<GURL, PlatformAuthProviderManager::GetDataCallback>
+      pending_callbacks;
+  EXPECT_CALL(*mock_provider, GetData(_, _))
+      .WillRepeatedly([&](const GURL& url,
+                          PlatformAuthProviderManager::GetDataCallback cb) {
+        pending_callbacks[url] = std::move(cb);
+        if (pending_callbacks.size() == 2) {
+          get_data_run_loop.Quit();
+        }
+      });
+
+  MockPlatformAuthProvider* unsafe_mock_provider = mock_provider.get();
+  ScopedSetProviderForTesting set_provider(std::move(mock_provider));
+
+  PlatformAuthProviderManager::GetInstance().SetEnabled(true,
+                                                        base::OnceClosure());
+
+  GURL url1 = embedded_test_server()->GetURL("/title1.html");
+  GURL url2 = embedded_test_server()->GetURL("/title2.html");
+
+  // Start navigation in tab 1.
+  content::WebContents* tab1 = chrome_test_utils::GetActiveWebContents(this);
+  tab1->GetController().LoadURL(url1, content::Referrer(),
+                                ui::PAGE_TRANSITION_TYPED, std::string());
+
+  // Open a second tab and start navigation.
+  chrome::AddTabAt(browser(), GURL("about:blank"), -1, true);
+  content::WebContents* tab2 = chrome_test_utils::GetActiveWebContents(this);
+  ASSERT_NE(tab1, tab2);
+  tab2->GetController().LoadURL(url2, content::Referrer(),
+                                ui::PAGE_TRANSITION_TYPED, std::string());
+
+  // Wait until both navigations have reached the provider's GetData().
+  get_data_run_loop.Run();
+
+  ASSERT_EQ(pending_callbacks.size(), 2u);
+  ASSERT_TRUE(pending_callbacks.find(url1) != pending_callbacks.end());
+  ASSERT_TRUE(pending_callbacks.find(url2) != pending_callbacks.end());
+
+  // Resolve request 1 with token 1.
+  net::HttpRequestHeaders headers1;
+  headers1.SetHeader(net::HttpRequestHeaders::kCookie, "token=Token1");
+  std::move(pending_callbacks[url1]).Run(std::move(headers1));
+
+  // Resolve request 2 with token 2.
+  net::HttpRequestHeaders headers2;
+  headers2.SetHeader(net::HttpRequestHeaders::kCookie, "token=Token2");
+  std::move(pending_callbacks[url2]).Run(std::move(headers2));
+
+  // Wait for both tabs to finish loading.
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.