Chrome · Enterprise
CVE-2026-87454
Logic Error in Enterprise
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/enterprise/platform_auth/cloud_ap_provider_win.cc |
modified | |
GURLchrome/browser/enterprise/platform_auth/cloud_ap_provider_win.h |
modified |
Files Changed
chrome/browser/enterprise/platform_auth/cloud_ap_provider_win.ccchrome/browser/enterprise/platform_auth/cloud_ap_provider_win.hchrome/browser/enterprise/platform_auth/cloud_ap_provider_win_unittest.cc
Patch
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/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"
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.
References
On This Page