Low watch CVSS 5.9 chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
5.9
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect provision of specified functionality in Proxy
DescriptionIncorrect provision of specified functionality in Proxy
ComponentProxy
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker533408915
Fix commitd19e2df40048 (chromium/src) +55/-16
CWECWE-684
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-10 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

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 set stores strings in sorted (alphabetical) order; a vector preserves 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.

Key insight
The single mistake was using a 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

  1. 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.
  2. Chrome enumerates adapters GetCandidateAdapterNames collects DHCP-capable adapter GUIDs; GetAdaptersAddresses() lists the preferred adapter first.
  3. 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.
  4. 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

The bug can cause Chrome to select a WPAD/PAC configuration from a non-preferred network adapter, so an attacker who controls DHCP WPAD on a lower-priority adapter could have their proxy configuration take effect instead of the OS-preferred network’s, undermining correct proxy provisioning on Windows. Preconditions are multiple DHCP-capable adapters where the attacker’s adapter GUID sorts favorably; the browser-process network stack applies the resulting proxy. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 5.9.

Changed Functions

FunctionChangeNotes
for
net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
modified
if
net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
modified
TEST
net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc
modified

Files Changed

  • net/proxy_resolution/win/dhcp_pac_file_fetcher_win.cc
  • net/proxy_resolution/win/dhcp_pac_file_fetcher_win.h
  • net/proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc

Audit Directions

  • Sorted containers for ordered data
    Audit 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 selection
    Review “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 config
    Examine 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.
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
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.