CVE-2026-106214
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/proxy_resolution/win/proxy_resolver_winhttp.cc |
modified | |
NET_EXPORT_PRIVATEnet/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h |
modified | |
ifnet/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc |
modified | |
ProxyResolverWinHttpAuthChallengeTestnet/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc |
modified | |
TEST_Pnet/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc |
modified | |
executablenet/BUILD.gn |
modified |
Files Changed
net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.hnet/proxy_resolution/win/proxy_resolver_winhttp_unittest.ccnet/proxy_resolution/win/proxy_resolver_winhttp.ccnet/BUILD.gn
Patch
From 6a2ce41c631e27dc6b9685e0d89dfbba6abbd4f4 Mon Sep 17 00:00:00 2001 From: Sebastien Lalancette <[email protected]> Date: Wed, 02 Sep 2026 06:19:22 -0700 Subject: [PATCH] [net] Stop PAC script auto-auth fetches in ProxyResolverWinHttp Removes the retry with `fAutoLogonIfChallenged = TRUE` in ProxyResolverWinHttp upon receiving ERROR_WINHTTP_LOGIN_FAILURE. The auto-logon fallback was legacy behavior for older Windows versions that has since been deprecated. Removing this retry aligns ProxyResolverWinHttp with modern Windows guidance (MS16-077) as well as the behavior of PacFileFetcherImpl and WindowsSystemProxyResolverImpl, neither of which attempts automatic logon when retrieving PAC scripts. Also adds unit tests verifying that an authentication challenge during PAC retrieval fails without retrying with auto-logon. Fixed: 533493992 Change-Id: I02be9263faded7790d38e3403af6e7425c3e351a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8288770 Commit-Queue: Sebastien Lalancette <[email protected]> Reviewed-by: Kenichi Ishibashi <[email protected]> Cr-Commit-Position: refs/heads/main@{#1690651} --- diff --git a/net/proxy_resolution/win/proxy_resolver_winhttp.cc b/net/proxy_resolution/win/proxy_resolver_winhttp.cc index fc47a724..5e2126f 100644 --- a/net/proxy_resolution/win/proxy_resolver_winhttp.cc +++ b/net/proxy_resolution/win/proxy_resolver_winhttp.cc @@ -16,6 +16,7 @@ #include "net/base/url_util.h" #include "net/proxy_resolution/proxy_info.h" #include "net/proxy_resolution/proxy_resolver.h" +#include "net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h" #include "url/gurl.h" using base::TimeTicks; @@ -23,6 +24,11 @@ namespace net { namespace { +// The WinHttpGetProxyForUrl implementation used by ProxyResolverWinHttp. May be +// overridden by tests via ScopedWinHttpGetProxyForUrlOverride. +WinHttpGetProxyForUrlFunc g_winhttp_get_proxy_for_url = + &::WinHttpGetProxyForUrl; + static void FreeInfo(WINHTTP_PROXY_INFO* info) { if (info->lpszProxy) GlobalFree(info->lpszProxy); @@ -117,6 +123,12 @@ // to avoid WinHTTP's auto-detection code, which while more featureful (it // supports DHCP based auto-detection) also appears to have issues. // + // The fAutoLogonIfChallenged option has been deprecated and should always be + // set to FALSE throughout Windows 10+. Even in earlier versions of the OS, + // this feature did not work particularly well (see MS16-077: + // https://support.microsoft.com/en-us/help/3161949/ms16-077-description-of-the-security-update-for-wpad-june-14-2016). + // PAC file fetches must not automatically provide default credentials if + // challenged. WINHTTP_AUTOPROXY_OPTIONS options = {0}; options.fAutoLogonIfChallenged = FALSE; options.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL; @@ -126,33 +138,19 @@ WINHTTP_PROXY_INFO info = {0}; DCHECK(session_handle_); - // Per http://msdn.microsoft.com/en-us/library/aa383153(VS.85).aspx, it is - // necessary to first try resolving with fAutoLogonIfChallenged set to false. - // Otherwise, we fail over to trying it with a value of true. This way we - // get good performance in the case where WinHTTP uses an out-of-process - // resolver. This is important for Vista and Win2k3. - BOOL ok = WinHttpGetProxyForUrl( + BOOL ok = g_winhttp_get_proxy_for_url( session_handle_, base::as_wcstr(base::ASCIIToUTF16(mutable_query_url.spec())), &options, &info); if (!ok) { - if (ERROR_WINHTTP_LOGIN_FAILURE == GetLastError()) { - options.fAutoLogonIfChallenged = TRUE; - ok = WinHttpGetProxyForUrl( - session_handle_, - base::as_wcstr(base::ASCIIToUTF16(mutable_query_url.spec())), - &options, &info); + DWORD error = GetLastError(); + // If we got here because of RPC timeout during out of process PAC + // resolution, no further requests on this session are going to work. + if (ERROR_WINHTTP_TIMEOUT == error || + ERROR_WINHTTP_AUTO_PROXY_SERVICE_ERROR == error) { + CloseWinHttpSession(); } - if (!ok) { - DWORD error = GetLastError(); - // If we got here because of RPC timeout during out of process PAC - // resolution, no further requests on this session are going to work. - if (ERROR_WINHTTP_TIMEOUT == error || - ERROR_WINHTTP_AUTO_PROXY_SERVICE_ERROR == error) { - CloseWinHttpSession(); - } - return WinHttpErrorToNetError(error); - } + return WinHttpErrorToNetError(error); } int rv = OK; @@ -226,4 +224,14 @@ return OK; } +ScopedWinHttpGetProxyForUrlOverride::ScopedWinHttpGetProxyForUrlOverride( + WinHttpGetProxyForUrlFunc func) + : previous_func_(g_winhttp_get_proxy_for_url) { + g_winhttp_get_proxy_for_url = func ? func : &::WinHttpGetProxyForUrl; +} + +ScopedWinHttpGetProxyForUrlOverride::~ScopedWinHttpGetProxyForUrlOverride() { + g_winhttp_get_proxy_for_url = previous_func_; +} + } // namespace net diff --git a/net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h b/net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h new file mode 100644 index 0000000..142b130 --- /dev/null +++ b/net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h @@ -0,0 +1,40 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_PROXY_RESOLUTION_WIN_PROXY_RESOLVER_WINHTTP_TEST_HOOKS_H_ +#define NET_PROXY_RESOLUTION_WIN_PROXY_RESOLVER_WINHTTP_TEST_HOOKS_H_ + +#include <windows.h> + +#include <winhttp.h> + +#include "net/base/net_export.h" + +namespace net { + +using WinHttpGetProxyForUrlFunc = BOOL(WINAPI*)(HINTERNET, + LPCWSTR, + WINHTTP_AUTOPROXY_OPTIONS*, + WINHTTP_PROXY_INFO*); + +// Scoped override of the WinHttpGetProxyForUrl function used by +// ProxyResolverWinHttp. Passing nullptr restores the default +// (::WinHttpGetProxyForUrl). +class NET_EXPORT_PRIVATE ScopedWinHttpGetProxyForUrlOverride { + public: + explicit ScopedWinHttpGetProxyForUrlOverride(WinHttpGetProxyForUrlFunc func); + ~ScopedWinHttpGetProxyForUrlOverride(); + + ScopedWinHttpGetProxyForUrlOverride( + const ScopedWinHttpGetProxyForUrlOverride&) = delete; + ScopedWinHttpGetProxyForUrlOverride& operator=( + const ScopedWinHttpGetProxyForUrlOverride&) = delete; + + private: + WinHttpGetProxyForUrlFunc previous_func_; +}; + +} // namespace net + +#endif // NET_PROXY_RESOLUTION_WIN_PROXY_RESOLVER_WINHTTP_TEST_HOOKS_H_ diff --git a/net/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc b/net/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc new file mode 100644 index 0000000..11fe1c4 --- /dev/null +++ b/net/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc @@ -0,0 +1,110 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/proxy_resolution/win/proxy_resolver_winhttp.h" + +#include <windows.h> + +#include <winhttp.h> + +#include <memory> +#include <string> + +#include "base/memory/scoped_refptr.h" +#include "net/base/net_errors.h" +#include "net/base/network_anonymization_key.h" +#include "net/log/net_log_with_source.h" +#include "net/proxy_resolution/pac_file_data.h" +#include "net/proxy_resolution/proxy_info.h" +#include "net/proxy_resolution/proxy_resolver.h" +#include "net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +int g_call_count = 0; +std::wstring g_expected_pac_url; + +BOOL WINAPI FakeGetProxyForUrlLoginFailure(HINTERNET /*session*/, + LPCWSTR /*url*/, + WINHTTP_AUTOPROXY_OPTIONS* options, + WINHTTP_PROXY_INFO* /*info*/) { + ++g_call_count; + EXPECT_NE(nullptr, options); + if (options) { + EXPECT_FALSE(options->fAutoLogonIfChallenged); + EXPECT_TRUE(options->dwFlags & WINHTTP_AUTOPROXY_CONFIG_URL); + if (options->lpszAutoConfigUrl) { + EXPECT_EQ(g_expected_pac_url, std::wstring(options->lpszAutoConfigUrl)); + } + } + ::SetLastError(ERROR_WINHTTP_LOGIN_FAILURE); + return FALSE; +} + +std::unique_ptr<ProxyResolver> CreateResolver( + const scoped_refptr<PacFileData>& script_data) { + ProxyResolverFactoryWinHttp factory; + std::unique_ptr<ProxyResolver> resolver; + std::unique_ptr<ProxyResolverFactory::Request> request; + int rv = factory.CreateProxyResolver(script_data, &resolver, + CompletionOnceCallback(), &request); + EXPECT_EQ(OK, rv); + return resolver; +} + +struct AuthChallengeTestCase { + scoped_refptr<PacFileData> script_data; + std::wstring expected_pac_url; +}; + +class ProxyResolverWinHttpAuthChallengeTest + : public testing::Test, + public testing::WithParamInterface<AuthChallengeTestCase> { + public: + void SetUp() override { + g_call_count = 0; + g_expected_pac_url.clear(); + } +}; + +} // namespace + +// If the server hosting the PAC script requests authentication when the +// resolver tries to download it, the resolver must not send default +// credentials and must instead surface a failure to the caller. This mirrors +// the behaviour of PacFileFetcherImpl on the non-WinHTTP code path. +TEST_P(ProxyResolverWinHttpAuthChallengeTest, NoAutoLogonOnChallenge) { + const AuthChallengeTestCase& test_case = GetParam(); + g_expected_pac_url = test_case.expected_pac_url; + ScopedWinHttpGetProxyForUrlOverride override_hook( + &FakeGetProxyForUrlLoginFailure); + + std::unique_ptr<ProxyResolver> resolver = + CreateResolver(test_case.script_data); + ASSERT_TRUE(resolver); + + ProxyInfo results; + std::unique_ptr<ProxyResolver::Request> request; + int rv = resolver->GetProxyForURL( + GURL("https://example.test/"), NetworkAnonymizationKey(), &results, + CompletionOnceCallback(), &request, NetLogWithSource()); + + EXPECT_EQ(ERR_PROXY_AUTH_UNSUPPORTED, rv); + EXPECT_EQ(1, g_call_count); +} + +INSTANTIATE_TEST_SUITE_P( + All, + ProxyResolverWinHttpAuthChallengeTest, + testing::Values(AuthChallengeTestCase{PacFileData::ForAutoDetect(), + L"http://wpad/wpad.dat"}, + AuthChallengeTestCase{ + PacFileData::FromURL(GURL("http://pac.test/wpad.dat")), + L"http://pac.test/wpad.dat"})); + +} // namespace net diff --git a/net/BUILD.gn b/net/BUILD.gn index 9db60a2..8bc28dd 100644 --- a/net/BUILD.gn +++ b/net/BUILD.gn @@ -1519,6 +1519,7 @@ "proxy_resolution/win/proxy_config_service_win.h", "proxy_resolution/win/proxy_resolver_winhttp.cc", "proxy_resolution/win/proxy_resolver_winhttp.h", + "proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h", "proxy_resolution/win/windows_system_proxy_resolution_request.cc", "proxy_resolution/win/windows_system_proxy_resolution_request.h", "proxy_resolution/win/windows_system_proxy_resolution_service.cc", @@ -2768,6 +2769,7 @@ "//url", ] } + executable("quic_server") { testonly = true sources = [ @@ -3281,6 +3283,7 @@ "proxy_resolution/win/dhcp_pac_file_adapter_fetcher_win_unittest.cc", "proxy_resolution/win/dhcp_pac_file_fetcher_win_unittest.cc", "proxy_resolution/win/proxy_config_service_win_unittest.cc", + "proxy_resolution/win/proxy_resolver_winhttp_unittest.cc", "proxy_resolution/win/windows_system_proxy_resolution_service_unittest.cc", "ssl/client_cert_store_win_unittest.cc", "ssl/ssl_platform_key_win_unittest.cc",
Regression Test / PoC
diff --git a/net/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc b/net/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc
new file mode 100644
index 0000000..11fe1c4
--- /dev/null
+++ b/net/proxy_resolution/win/proxy_resolver_winhttp_unittest.cc
@@ -0,0 +1,110 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/proxy_resolution/win/proxy_resolver_winhttp.h"
+
+#include <windows.h>
+
+#include <winhttp.h>
+
+#include <memory>
+#include <string>
+
+#include "base/memory/scoped_refptr.h"
+#include "net/base/net_errors.h"
+#include "net/base/network_anonymization_key.h"
+#include "net/log/net_log_with_source.h"
+#include "net/proxy_resolution/pac_file_data.h"
+#include "net/proxy_resolution/proxy_info.h"
+#include "net/proxy_resolution/proxy_resolver.h"
+#include "net/proxy_resolution/win/proxy_resolver_winhttp_test_hooks.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace net {
+
+namespace {
+
+int g_call_count = 0;
+std::wstring g_expected_pac_url;
+
+BOOL WINAPI FakeGetProxyForUrlLoginFailure(HINTERNET /*session*/,
+ LPCWSTR /*url*/,
+ WINHTTP_AUTOPROXY_OPTIONS* options,
+ WINHTTP_PROXY_INFO* /*info*/) {
+ ++g_call_count;
+ EXPECT_NE(nullptr, options);
+ if (options) {
+ EXPECT_FALSE(options->fAutoLogonIfChallenged);
+ EXPECT_TRUE(options->dwFlags & WINHTTP_AUTOPROXY_CONFIG_URL);
+ if (options->lpszAutoConfigUrl) {
+ EXPECT_EQ(g_expected_pac_url, std::wstring(options->lpszAutoConfigUrl));
+ }
+ }
+ ::SetLastError(ERROR_WINHTTP_LOGIN_FAILURE);
+ return FALSE;
+}
+
+std::unique_ptr<ProxyResolver> CreateResolver(
+ const scoped_refptr<PacFileData>& script_data) {
+ ProxyResolverFactoryWinHttp factory;
+ std::unique_ptr<ProxyResolver> resolver;
+ std::unique_ptr<ProxyResolverFactory::Request> request;
+ int rv = factory.CreateProxyResolver(script_data, &resolver,
+ CompletionOnceCallback(), &request);
+ EXPECT_EQ(OK, rv);
+ return resolver;
+}
+
+struct AuthChallengeTestCase {
+ scoped_refptr<PacFileData> script_data;
+ std::wstring expected_pac_url;
+};
+
+class ProxyResolverWinHttpAuthChallengeTest
+ : public testing::Test,
+ public testing::WithParamInterface<AuthChallengeTestCase> {
+ public:
+ void SetUp() override {
+ g_call_count = 0;
+ g_expected_pac_url.clear();
+ }
+};
+
+} // namespace
+
+// If the server hosting the PAC script requests authentication when the
+// resolver tries to download it, the resolver must not send default
+// credentials and must instead surface a failure to the caller. This mirrors
+// the behaviour of PacFileFetcherImpl on the non-WinHTTP code path.
+TEST_P(ProxyResolverWinHttpAuthChallengeTest, NoAutoLogonOnChallenge) {
+ const AuthChallengeTestCase& test_case = GetParam();
+ g_expected_pac_url = test_case.expected_pac_url;
+ ScopedWinHttpGetProxyForUrlOverride override_hook(
+ &FakeGetProxyForUrlLoginFailure);
+
+ std::unique_ptr<ProxyResolver> resolver =
+ CreateResolver(test_case.script_data);
+ ASSERT_TRUE(resolver);
+
+ ProxyInfo results;
+ std::unique_ptr<ProxyResolver::Request> request;
+ int rv = resolver->GetProxyForURL(
+ GURL("https://example.test/"), NetworkAnonymizationKey(), &results,
+ CompletionOnceCallback(), &request, NetLogWithSource());
+
+ EXPECT_EQ(ERR_PROXY_AUTH_UNSUPPORTED, rv);
+ EXPECT_EQ(1, g_call_count);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ All,
+ ProxyResolverWinHttpAuthChallengeTest,
+ testing::Values(AuthChallengeTestCase{PacFileData::ForAutoDetect(),
+ L"http://wpad/wpad.dat"},
+ AuthChallengeTestCase{
+ PacFileData::FromURL(GURL("http://pac.test/wpad.dat")),
+ L"http://pac.test/wpad.dat"}));
+
+} // namespace net
Original Bug Report
Potential NTLM credential leak in ProxyResolverWinHttp via fAutoLogonIfChallenged retry
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: When handling PAC file fetches via ProxyResolverWinHttp, receiving an HTTP 401 challenge triggers an explicit retry with fAutoLogonIfChallenged set to TRUE. This causes WinHTTP to automatically transmit the local Windows user’s NetNTLMv2 credentials to an attacker-controlled wpad host, bypassing standard auth-canceling protections.
Affected files:
net/proxy_resolution/win/proxy_resolver_winhttp.ccnet/proxy_resolution/pac_file_decider.ccchrome/browser/net/system_network_context_manager.ccservices/network/url_request_context_builder_mojo.ccnet/url_request/url_request_context_builder.ccnet/proxy_resolution/configured_proxy_resolution_service.cc
Estimated timestamp from git blame: 2008-09-09
1. Summary of the Issue (Meant for Human Triage)
A potential High-severity credential exposure vulnerability (NET-CRED) exists in Chrome’s Windows proxy resolution logic when using the WinHTTP proxy resolver. Chromium’s security guidelines strictly dictate that PAC script fetches must never send credentials (a rule typically enforced by PacFileFetcherImpl::OnAuthRequired on the default V8-Mojo path).
However, when the ProxyResolverWinHttp implementation is utilized, it bypasses the standard PacFileFetcherImpl logic entirely because it explicitly declares expects_pac_bytes = false. When the system attempts to fetch the PAC file using the Windows API WinHttpGetProxyForUrl and receives an HTTP 401 challenge (yielding ERROR_WINHTTP_LOGIN_FAILURE), the resolver catches this error and explicitly retries the request with fAutoLogonIfChallenged = TRUE.
Because Chromium does not override the default WINHTTP_OPTION_AUTOLOGON_POLICY, WinHTTP falls back to its default WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM behavior. Under this policy, WinHTTP treats the standard, dotless wpad hostname as belonging to the Intranet zone. Consequently, setting fAutoLogonIfChallenged = TRUE instructs WinHTTP to automatically and silently transmit the local interactive user’s Windows OS credentials (NetNTLMv2 hash) to whoever answers the wpad resolution. An attacker positioned on the local network (e.g., rogue access point, coffee-shop Wi-Fi, or via LLMNR/DNS spoofing) can answer this query, serve a 401 challenge, and seamlessly harvest the victim’s domain credentials for offline cracking or NTLM relay attacks.
Reachability: In official Chrome releases, this vulnerability is reachable when launched with the --winhttp-proxy-resolver or --single-process command-line flags. While off-by-default in Chrome proper, severity guidelines require assessing this as-if-reachable (High/S1) with Security_Impact-None. Crucially, this vulnerable path is the default out-of-the-box production behavior for third-party Windows applications that embed Chromium’s //net library and use the default net::URLRequestContextBuilder::CreateProxyResolutionService.
Note: The exploitation steps detailed below are suggested/potential steps, as our tooling agent does not currently have the ability to run external code to provide a live proof-of-concept.
2. Proof-of-Concept & Detailed Execution Flow
The following traces the potential execution flow from attacker interception to credential compromise.
Attacker Prerequisites
- The attacker is positioned on the same local network as the victim (e.g., A-NETCONF threat model) and runs a DNS/LLMNR spoofer to answer
wpadqueries with their own IP, alongside a malicious HTTP server on port 80. - The Windows victim has “Automatically detect settings” enabled in Internet Options and either runs Chrome with
--winhttp-proxy-resolver, or uses a third-party//netembedder on Windows.
Execution Trace
Step 1: Network Context Initialization Fallback
When launched with the flag, SystemNetworkContextManager::CreateNetworkContextParams skips setting the Mojo proxy_resolver_factory (chrome/browser/net/system_network_context_manager.cc:976-980). This causes URLRequestContextBuilderMojo::CreateProxyResolutionService to fall back to the standard net::URLRequestContextBuilder::CreateProxyResolutionService (services/network/url_request_context_builder_mojo.cc:116).
Step 2: Null Fetcher Injection
On this fallback path, ConfiguredProxyResolutionService::SetPacFileFetchers is never called. As a result, the service’s pac_file_fetcher_ remains nullptr. The service initializes using ProxyResolverFactoryForSystem (net/proxy_resolution/configured_proxy_resolution_service.cc:861), which on Windows instantiates ProxyResolverFactoryWinHttp (net/proxy_resolution/configured_proxy_resolution_service.cc:253-255).
Step 3: Disabling Safe PAC Byte Fetching
ProxyResolverFactoryWinHttp calls its base class constructor with expects_pac_bytes = false:
// net/proxy_resolution/win/proxy_resolver_winhttp.cc:216-218
ProxyResolverFactoryWinHttp::ProxyResolverFactoryWinHttp()
: ProxyResolverFactory(false /*expects_pac_bytes*/) {}
When the proxy service initializes, it invokes InitProxyResolver::DoDecidePacFile, which extracts this boolean flag and passes it to PacFileDecider::Start (net/proxy_resolution/configured_proxy_resolution_service.cc:551). PacFileDecider saves this to fetch_pac_bytes_ = false.
Step 4: PacFileDecider Skips Mitigation
Because pac_file_fetcher_ is null, PacFileDecider::DoQuickCheck skips the WPAD quick-check and jumps to GetStartState() (net/proxy_resolution/pac_file_decider.cc:255-260). In GetStartState(), because fetch_pac_bytes_ is false, it returns STATE_VERIFY_PAC_SCRIPT instead of STATE_FETCH_PAC_SCRIPT:
// net/proxy_resolution/pac_file_decider.cc:446-448
PacFileDecider::State PacFileDecider::GetStartState() const {
return fetch_pac_bytes_ ? STATE_FETCH_PAC_SCRIPT : STATE_VERIFY_PAC_SCRIPT;
}
This completely bypasses PacFileFetcherImpl::OnAuthRequired, circumventing Chrome’s primary mechanism for safely aborting authenticated PAC fetches.
Step 5: WPAD URL Hardcoding
PacFileDecider yields PacFileData::ForAutoDetect(). The ProxyResolverWinHttp constructor sees TYPE_AUTO_DETECT and hardcodes its internal pac_url_ to http://wpad/wpad.dat:
// net/proxy_resolution/win/proxy_resolver_winhttp.cc:82-86
ProxyResolverWinHttp::ProxyResolverWinHttp(
const scoped_refptr<PacFileData>& script_data)
: pac_url_(script_data->type() == PacFileData::TYPE_AUTO_DETECT
? GURL("http://wpad/wpad.dat")
: script_data->url()) {}
Step 6: First Failed OS Fetch Attempt
When a network request is made, ProxyResolverWinHttp::GetProxyForURL begins proxy resolution. It initializes WINHTTP_AUTOPROXY_OPTIONS with fAutoLogonIfChallenged = FALSE and calls the Windows OS API:
// net/proxy_resolution/win/proxy_resolver_winhttp.cc:121-137
options.fAutoLogonIfChallenged = FALSE;
// ... string conversions ...
BOOL ok = WinHttpGetProxyForUrl(session_handle_, ..., &options, &info);
The OS requests http://wpad/wpad.dat from the attacker. The attacker responds with HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: NTLM. WinHTTP rejects it, returning FALSE and yielding ERROR_WINHTTP_LOGIN_FAILURE.
Step 7: The Vulnerable Credential-Leaking Retry
The resolver traps this error and explicitly toggles fAutoLogonIfChallenged to TRUE, initiating a retry:
// net/proxy_resolution/win/proxy_resolver_winhttp.cc:138-144
if (!ok) {
if (ERROR_WINHTTP_LOGIN_FAILURE == GetLastError()) {
options.fAutoLogonIfChallenged = TRUE; // <--- The Vulnerability
ok = WinHttpGetProxyForUrl(session_handle_, ..., &options, &info);
}
Because Chrome never configures WINHTTP_OPTION_AUTOLOGON_POLICY on the session handle (net/proxy_resolution/win/proxy_resolver_winhttp.cc:189-205), WinHTTP uses the default WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM. Under this policy, WinHTTP inherently trusts dotless hostnames like wpad as belonging to the Intranet zone. Consequently, WinHTTP immediately and silently responds to the attacker’s NTLM challenge with the current Windows user’s NetNTLMv2 hash, resulting in domain credential compromise.
Suggested Fix
The explicit retry block starting at net/proxy_resolution/win/proxy_resolver_winhttp.cc:138 should be removed, or alternatively, fAutoLogonIfChallenged should remain statically false. If there is a legitimate legacy requirement for this retry, it must be gated behind strict same-zone or HTTPS-only policy checks, or the session must be locked down by explicitly setting WINHTTP_OPTION_AUTOLOGON_POLICY to WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH to prevent arbitrary dotless hostname trust.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
Prior Critic Verdict:
* Severity: High (S1)
* Brief Notes / Reasoning:
The report correctly identifies a logic flaw in `ProxyResolverWinHttp` where Chrome explicitly retries a failed PAC fetch with `fAutoLogonIfChallenged = TRUE` upon receiving a 401 challenge (`net/proxy_resolution/win/proxy_resolver_winhttp.cc:140`). This violates the security invariant enforced on the default V8-mojo path, which explicitly cancels auth for PAC fetches to prevent credential leaks. An attacker on the local network (MITM) who spoof the `wpad` host and returns an NTLM challenge can force WinHTTP to automatically send the victim's Windows OS credentials (NetNTLMv2 hash), as WinHTTP treats dotless hostnames as Intranet-zone by default. This allows for offline cracking or NTLM relay attacks, directly matching the High (S1) criteria: 'HTTP auth credentials ... is sent to an origin other than the one it is bound to'. Although this path is only reachable in Chrome via the off-by-default `--winhttp-proxy-resolver` flag, the severity guidelines explicitly state: 'assess severity AS IF the flag were on, and note Security_Impact-None separately. Do NOT downgrade to S3 for unreachability'. Therefore, the as-if-reachable severity remains S1. Note: `Security_Impact-None` applies for default Chrome, though this is the default production behavior for any third-party Windows `//net` embedders using `URLRequestContextBuilder::CreateProxyResolutionService`.
Codebase Investigator Proofs:
- Propagation of
expects_pac_bytes(): Trace confirms thatProxyResolverFactoryWinHttpinitializesexpects_pac_bytestofalse. WhenConfiguredProxyResolutionService::InitializeUsingLastFetchedConfiginvokesinit_proxy_resolver_->Start, the decider is started usingdecider_->Start(..., proxy_resolver_factory_->expects_pac_bytes(), ...)(line 551). This passesfalsetoPacFileDecider::Start, storing it infetch_pac_bytes_(line 113).PacFileDecider::GetStartState(lines 446-448) returnsSTATE_VERIFY_PAC_SCRIPT, circumventing the fetch phase. - State of
pac_file_fetcher_: The investigator verified thatConfiguredProxyResolutionService::SetPacFileFetchersis only called withinservices/network/proxy_service_mojo.cc. On the fallback path throughnet::URLRequestContextBuilder::CreateProxyResolutionService(called whenmojo_proxy_resolver_factory_is null),SetPacFileFetchersis completely skipped, leavingpac_file_fetcher_permanentlynullptr. - Bypass of
DoQuickCheck: Investigator logs verify thatPacFileDecider::DoQuickCheck(lines 255-260) explicitly checks!pac_file_fetcher_. Because it is null on this path, it skips the check entirely and transitions viaGetStartState(). - PAC URL and
fAutoLogonIfChallengedSync: Investigator verifies thatProxyResolverWinHttpevaluatesscript_data->type() == PacFileData::TYPE_AUTO_DETECTand hardcodesGURL("http://wpad/wpad.dat"). InGetProxyForURL(lines 120-144), this URL is assigned tooptions.lpszAutoConfigUrl. It confirms that the fallback execution (line 140) setsoptions.fAutoLogonIfChallenged = TRUEand immediately callsWinHttpGetProxyForUrl.
Environmental Assumptions & Requirements:
WINHTTP_OPTION_AUTOLOGON_POLICYremains unset (verified via lack of references innet/proxy_resolution/win/proxy_resolver_winhttp.cc:189-205), meaningWINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUMis active.- The bare, dotless hostname
wpadsatisfies Windows Intranet Zone checks for automatic NTLM/Negotiate authentication under the MEDIUM security level. --winhttp-proxy-resolvercommand line switch remains actively evaluated for system fallback, or the application embeds the//netAPI defaults directly.
Evaluated with Chrome root at commit: f4cb78b4ec077b7f51b504af9350cdc166d10c2f
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.