CVE-2026-79126
Overview
Background
- WPAD / DHCP PAC
- Web Proxy Auto-Discovery via DHCP, where Chrome queries network adapters’ DHCP servers for a PAC (proxy auto-config) URL to decide the proxy for outgoing traffic.
- `DhcpPacFileFetcherWin`
- the Windows implementation that enumerates candidate DHCP-capable adapters and fetches their PAC URLs, using the first successful (most-preferred) adapter.
- `GetAdaptersAddresses()` order
- the Windows API returns adapters in route-metric preference order, so the first DHCP-capable adapter is the one the OS most prefers for routing.
- `std::set<std::string>` vs `std::vector<std::string>`
- a
setstores strings in sorted (alphabetical) order; avectorpreserves insertion order.
Root Cause Analysis
DhcpPacFileFetcherWin::GetCandidateAdapterNames collected candidate adapter GUID names into a std::set<std::string>, which sorted them alphabetically and discarded the route-metric preference order in which GetAdaptersAddresses() had returned them. Because the fetcher consumes adapter_names() in order and short-circuits on the first adapter that yields a PAC URL, sorting the GUIDs meant Chrome could select the PAC configuration from a non-preferred adapter (whichever adapter’s GUID happened to sort first) rather than the OS’s highest-priority network. This violates the intended invariant that the most-preferred adapter’s WPAD result wins.
The fix replaces the set with a std::vector<std::string> throughout (GetCandidateAdapterNames, AdapterQuery::adapter_names(), ImplGetCandidateAdapterNames, and the adapter_names_ member), preserving enumeration order while adding an explicit std::ranges::find dedup check before push_back to retain the set’s uniqueness guarantee. The regression test PreservesCandidateAdapterOrder inserts z_primary before a_secondary and asserts the primary (first-enumerated) adapter is fetched first and supplies the resulting PAC.
std::set to hold adapter names, whose alphabetical sorting silently discarded the OS’s route-metric preference order and let a less-preferred adapter’s WPAD/PAC result be chosen; the fix stores names in an order-preserving std::vector (with manual dedup).Attack Path
- Attacker positioned on a lower-priority network The victim machine has multiple DHCP-capable adapters, and an attacker controls the DHCP/WPAD response on one that is not the OS’s preferred adapter.
- Chrome enumerates adapters
GetCandidateAdapterNamescollects DHCP-capable adapter GUIDs;GetAdaptersAddresses()lists the preferred adapter first. - Order lost via `std::set` Pre-patch, the names are re-sorted alphabetically, so the attacker’s adapter GUID can sort ahead of the legitimately preferred adapter’s GUID.
- Wrong PAC selected The fetcher short-circuits on the first adapter in the (mis)ordered list, adopting the attacker’s PAC URL and thus the attacker’s proxy configuration for the victim’s traffic (traffic interception/MITM via the chosen proxy is inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
fornet/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc |
modified | |
ifnet/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc |
modified | |
TESTnet/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc |
modified |
Files Changed
net/proxy_resolution/win/dhcp_pac_file_fetcher_win.ccnet/proxy_resolution/win/dhcp_pac_file_fetcher_win.hnet/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
Audit Directions
- Sorted containers for ordered dataAudit code that stores OS- or protocol-supplied priority-ordered lists (adapters, servers, proxies, certificates) in
std::set/std::map, which silently re-sorts and destroys the intended selection order. - First-match / short-circuit selectionReview “use the first candidate that succeeds” logic to confirm the candidate ordering fed to it reflects the true preference metric rather than an incidental sort.
- Multi-adapter / multi-source network configExamine WPAD, DNS, and proxy discovery paths on multi-homed machines for cases where a lower-priority or attacker-influenced source can supersede the preferred one due to ordering bugs.
Patch
From d19e2df40048b0031919e698917d19abae20d856 Mon Sep 17 00:00:00 2001 From: Patrick Meenan <[email protected]> Date: Tue, 21 Jul 2026 07:04:49 -0700 Subject: [PATCH] Maintain adapter priority order for Windows WPAD DhcpPacFileFetcherWin previously stored candidate network adapter names in a std::set<std::string>. This sorted the adapter GUID strings alphabetically, losing the route metric preference order returned by GetAdaptersAddresses(). This change replaces the set with a vector to maintain the adapter priority order. Bug: 533408915 Change-Id: Ied4a9b255f62feed0a4823b0bde2c673ba1f339e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8087629 Reviewed-by: Nidhi Jaju <[email protected]> Commit-Queue: Patrick Meenan <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665448} --- diff --git a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.cc b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.cc index 866efaf7..cbd8341c 100644 --- a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.cc +++ b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.cc @@ -8,6 +8,7 @@ #include <iphlpapi.h> +#include <algorithm> #include <memory> #include <vector> @@ -370,7 +371,7 @@ state_ = STATE_NO_RESULTS; - const std::set<std::string>& adapter_names = query->adapter_names(); + const std::vector<std::string>& adapter_names = query->adapter_names(); if (adapter_names.empty()) { TransitionToDone(); @@ -525,7 +526,7 @@ } bool DhcpPacFileFetcherWin::GetCandidateAdapterNames( - std::set<std::string>* adapter_names, + std::vector<std::string>* adapter_names, DhcpAdapterNamesLoggingInfo* info) { DCHECK(adapter_names); adapter_names->clear(); @@ -567,7 +568,10 @@ for (adapter = adapters.get(); adapter; adapter = adapter->Next) { if (IsDhcpCapableAdapter(adapter)) { DCHECK(adapter->AdapterName); - adapter_names->insert(adapter->AdapterName); + if (std::ranges::find(*adapter_names, adapter->AdapterName) == + adapter_names->end()) { + adapter_names->push_back(adapter->AdapterName); + } } } @@ -591,13 +595,13 @@ logging_info_->worker_thread_end_time = base::TimeTicks::Now(); } -const std::set<std::string>& +const std::vector<std::string>& DhcpPacFileFetcherWin::AdapterQuery::adapter_names() const { return adapter_names_; } bool DhcpPacFileFetcherWin::AdapterQuery::ImplGetCandidateAdapterNames( - std::set<std::string>* adapter_names, + std::vector<std::string>* adapter_names, DhcpAdapterNamesLoggingInfo* info) { return DhcpPacFileFetcherWin::GetCandidateAdapterNames(adapter_names, info); diff --git a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.h b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.h index 73a953f..adba861 100644 --- a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.h +++ b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win.h @@ -60,7 +60,7 @@ // this machine that has DHCP enabled and is not a loop-back adapter. May // optionally update |info| (if non-null) with information for logging. // Returns false on error. - static bool GetCandidateAdapterNames(std::set<std::string>* adapter_names, + static bool GetCandidateAdapterNames(std::vector<std::string>* adapter_names, DhcpAdapterNamesLoggingInfo* info); protected: @@ -84,16 +84,16 @@ // This is the method that runs on the worker pool thread. void GetCandidateAdapterNames(); - // This set is valid after GetCandidateAdapterNames has + // This vector is valid after GetCandidateAdapterNames has // been run. Its lifetime is scoped by this object. - const std::set<std::string>& adapter_names() const; + const std::vector<std::string>& adapter_names() const; DhcpAdapterNamesLoggingInfo* logging_info() { return logging_info_.get(); } protected: // Virtual method introduced to allow unit testing. virtual bool ImplGetCandidateAdapterNames( - std::set<std::string>* adapter_names, + std::vector<std::string>* adapter_names, DhcpAdapterNamesLoggingInfo* info); friend class base::RefCountedThreadSafe<AdapterQuery>; @@ -103,7 +103,7 @@ // These are constructed on the originating thread, then used on the // worker thread, then used again on the originating thread only when // the task has completed on the worker thread. No locking required. - std::set<std::string> adapter_names_; + std::vector<std::string> adapter_names_; std::unique_ptr<DhcpAdapterNamesLoggingInfo> logging_info_; }; diff --git a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc index 44bf5c7..76cf2ca 100644 --- a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc +++ b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc @@ -9,6 +9,7 @@ #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" +#include "base/memory/raw_ptr.h" #include "base/rand_util.h" #include "base/run_loop.h" #include "base/test/task_environment.h" @@ -41,7 +42,7 @@ // running in, so it just exercises the code to make sure there // is no crash and no error returned, but does not assert on the number // of interfaces or the information returned via DHCP. - std::set<std::string> adapter_names; + std::vector<std::string> adapter_names; DhcpPacFileFetcherWin::GetCandidateAdapterNames(&adapter_names, nullptr); for (const std::string& adapter_name : adapter_names) { DhcpPacFileAdapterFetcher::GetPacURLFromDhcp(adapter_name); @@ -222,6 +223,9 @@ void Fetch(const std::string& adapter_name, CompletionOnceCallback callback, const NetworkTrafficAnnotationTag traffic_annotation) override { + if (fetched_adapter_names_) { + fetched_adapter_names_->push_back(adapter_name); + } callback_ = std::move(callback); timer_.Start(FROM_HERE, base::Milliseconds(fetch_delay_ms_), this, &DummyDhcpPacFileAdapterFetcher::OnTimer); @@ -246,11 +250,13 @@ void Configure(bool did_finish, int result, std::u16string pac_script, - int fetch_delay_ms) { + int fetch_delay_ms, + std::vector<std::string>* fetched_adapter_names = nullptr) { did_finish_ = did_finish; result_ = result; pac_script_ = pac_script; fetch_delay_ms_ = fetch_delay_ms; + fetched_adapter_names_ = fetched_adapter_names; } private: @@ -258,6 +264,7 @@ int result_ = OK; std::u16string pac_script_; int fetch_delay_ms_ = 1; + raw_ptr<std::vector<std::string>> fetched_adapter_names_ = nullptr; CompletionOnceCallback callback_; base::OneShotTimer timer_; }; @@ -270,9 +277,9 @@ } bool ImplGetCandidateAdapterNames( - std::set<std::string>* adapter_names, + std::vector<std::string>* adapter_names, DhcpAdapterNamesLoggingInfo* logging) override { - adapter_names->insert(mock_adapter_names_.begin(), + adapter_names->insert(adapter_names->end(), mock_adapter_names_.begin(), mock_adapter_names_.end()); return true; } @@ -311,8 +318,9 @@ base::TimeDelta fetch_delay) { auto adapter_fetcher = std::make_unique<DummyDhcpPacFileAdapterFetcher>( url_request_context(), GetTaskRunner()); - adapter_fetcher->Configure( - did_finish, result, pac_script, fetch_delay.InMilliseconds()); + adapter_fetcher->Configure(did_finish, result, pac_script, + fetch_delay.InMilliseconds(), + &fetched_adapter_names_); PushBackAdapter(adapter_name, std::move(adapter_fetcher)); } @@ -336,9 +344,12 @@ } void ResetTestState() { + Cancel(); + worker_finished_event_.Reset(); next_adapter_fetcher_index_ = 0; num_fetchers_created_ = 0; adapter_fetchers_.clear(); + fetched_adapter_names_.clear(); adapter_query_ = base::MakeRefCounted<MockAdapterQuery>(); max_wait_ = TestTimeouts::tiny_timeout(); } @@ -349,6 +360,9 @@ int next_adapter_fetcher_index_; + // Must outlive adapter fetchers, which may hold pointers to this vector. + std::vector<std::string> fetched_adapter_names_; + // Ownership gets transferred to the implementation class via // ImplCreateAdapterFetcher, but any objects not handed out are // deleted on destruction. @@ -590,6 +604,26 @@ timer.Elapsed()); } +void TestPreservesCandidateAdapterOrder(FetcherClient* client) { + client->fetcher_.ConfigureAndPushBackAdapter( + "z_primary", true, OK, u"primary", base::Milliseconds(1)); + client->fetcher_.ConfigureAndPushBackAdapter( + "a_secondary", true, OK, u"secondary", base::Milliseconds(1)); + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(2u, client->fetcher_.fetched_adapter_names_.size()); + EXPECT_EQ("z_primary", client->fetcher_.fetched_adapter_names_[0]); + EXPECT_EQ("a_secondary", client->fetcher_.fetched_adapter_names_[1]); + EXPECT_EQ(u"primary", client->pac_text_); +} + +TEST(DhcpPacFileFetcherWin, PreservesCandidateAdapterOrder) { + base::test::TaskEnvironment task_environment; + + FetcherClient client; + TestPreservesCandidateAdapterOrder(&client); +} + TEST(DhcpPacFileFetcherWin, ShortCircuitLessPreferredAdapters) { base::test::TaskEnvironment task_environment; @@ -640,6 +674,7 @@ test_functions.push_back(TestFailureCaseNoDhcpAdapters); test_functions.push_back(TestShortCircuitLessPreferredAdapters); test_functions.push_back(TestImmediateCancel); + test_functions.push_back(TestPreservesCandidateAdapterOrder); base::RandomShuffle(test_functions.begin(), test_functions.end()); for (TestVector::const_iterator it = test_functions.begin();
Regression Test / PoC
diff --git a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
index 44bf5c7..76cf2ca 100644
--- a/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
+++ b/net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
@@ -9,6 +9,7 @@
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
+#include "base/memory/raw_ptr.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
@@ -41,7 +42,7 @@
// running in, so it just exercises the code to make sure there
// is no crash and no error returned, but does not assert on the number
// of interfaces or the information returned via DHCP.
- std::set<std::string> adapter_names;
+ std::vector<std::string> adapter_names;
DhcpPacFileFetcherWin::GetCandidateAdapterNames(&adapter_names, nullptr);
for (const std::string& adapter_name : adapter_names) {
DhcpPacFileAdapterFetcher::GetPacURLFromDhcp(adapter_name);
@@ -222,6 +223,9 @@
void Fetch(const std::string& adapter_name,
CompletionOnceCallback callback,
const NetworkTrafficAnnotationTag traffic_annotation) override {
+ if (fetched_adapter_names_) {
+ fetched_adapter_names_->push_back(adapter_name);
+ }
callback_ = std::move(callback);
timer_.Start(FROM_HERE, base::Milliseconds(fetch_delay_ms_), this,
&DummyDhcpPacFileAdapterFetcher::OnTimer);
@@ -246,11 +250,13 @@
void Configure(bool did_finish,
int result,
std::u16string pac_script,
- int fetch_delay_ms) {
+ int fetch_delay_ms,
+ std::vector<std::string>* fetched_adapter_names = nullptr) {
did_finish_ = did_finish;
result_ = result;
pac_script_ = pac_script;
fetch_delay_ms_ = fetch_delay_ms;
+ fetched_adapter_names_ = fetched_adapter_names;
}
private:
@@ -258,6 +264,7 @@
int result_ = OK;
std::u16string pac_script_;
int fetch_delay_ms_ = 1;
+ raw_ptr<std::vector<std::string>> fetched_adapter_names_ = nullptr;
CompletionOnceCallback callback_;
base::OneShotTimer timer_;
};
@@ -270,9 +277,9 @@
}
bool ImplGetCandidateAdapterNames(
- std::set<std::string>* adapter_names,
+ std::vector<std::string>* adapter_names,
DhcpAdapterNamesLoggingInfo* logging) override {
- adapter_names->insert(mock_adapter_names_.begin(),
+ adapter_names->insert(adapter_names->end(), mock_adapter_names_.begin(),
mock_adapter_names_.end());
return true;
}
@@ -311,8 +318,9 @@
base::TimeDelta fetch_delay) {
auto adapter_fetcher = std::make_unique<DummyDhcpPacFileAdapterFetcher>(
url_request_context(), GetTaskRunner());
- adapter_fetcher->Configure(
- did_finish, result, pac_script, fetch_delay.InMilliseconds());
+ adapter_fetcher->Configure(did_finish, result, pac_script,
+ fetch_delay.InMilliseconds(),
+ &fetched_adapter_names_);
PushBackAdapter(adapter_name, std::move(adapter_fetcher));
}
@@ -336,9 +344,12 @@
}
void ResetTestState() {
+ Cancel();
+ worker_finished_event_.Reset();
next_adapter_fetcher_index_ = 0;
num_fetchers_created_ = 0;
adapter_fetchers_.clear();
+ fetched_adapter_names_.clear();
adapter_query_ = base::MakeRefCounted<MockAdapterQuery>();
max_wait_ = TestTimeouts::tiny_timeout();
}
@@ -349,6 +360,9 @@
int next_adapter_fetcher_index_;
+ // Must outlive adapter fetchers, which may hold pointers to this vector.
+ std::vector<std::string> fetched_adapter_names_;
+
// Ownership gets transferred to the implementation class via
// ImplCreateAdapterFetcher, but any objects not handed out are
// deleted on destruction.
@@ -590,6 +604,26 @@
timer.Elapsed());
}
+void TestPreservesCandidateAdapterOrder(FetcherClient* client) {
+ client->fetcher_.ConfigureAndPushBackAdapter(
+ "z_primary", true, OK, u"primary", base::Milliseconds(1));
+ client->fetcher_.ConfigureAndPushBackAdapter(
+ "a_secondary", true, OK, u"secondary", base::Milliseconds(1));
+ client->RunTest();
+ client->RunMessageLoopUntilComplete();
+ ASSERT_EQ(2u, client->fetcher_.fetched_adapter_names_.size());
+ EXPECT_EQ("z_primary", client->fetcher_.fetched_adapter_names_[0]);
+ EXPECT_EQ("a_secondary", client->fetcher_.fetched_adapter_names_[1]);
+ EXPECT_EQ(u"primary", client->pac_text_);
+}
+
+TEST(DhcpPacFileFetcherWin, PreservesCandidateAdapterOrder) {
+ base::test::TaskEnvironment task_environment;
+
+ FetcherClient client;
+ TestPreservesCandidateAdapterOrder(&client);
+}
+
TEST(DhcpPacFileFetcherWin, ShortCircuitLessPreferredAdapters) {
base::test::TaskEnvironment task_environment;
@@ -640,6 +674,7 @@
test_functions.push_back(TestFailureCaseNoDhcpAdapters);
test_functions.push_back(TestShortCircuitLessPreferredAdapters);
test_functions.push_back(TestImmediateCancel);
+ test_functions.push_back(TestPreservesCandidateAdapterOrder);
base::RandomShuffle(test_functions.begin(), test_functions.end());
for (TestVector::const_iterator it = test_functions.begin();