CVE-2026-79034
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/cors/cors_url_loader.cc |
modified | |
TEST_Fservices/network/cors/cors_url_loader_unittest.cc |
modified |
Files Changed
services/network/cors/cors_url_loader.ccservices/network/cors/cors_url_loader_test_util.ccservices/network/cors/cors_url_loader_test_util.hservices/network/cors/cors_url_loader_unittest.cc
Patch
From 9be50510944a7e4899786ba7a1012dd2e3293510 Mon Sep 17 00:00:00 2001
From: amarjotgill <amarjotgill@google.com>
Date: Fri, 10 Jul 2026 06:18:53 -0700
Subject: [PATCH] Sanitize the did_use_server_http_auth field inside CorsURLLoader
Bug: 532957785
Change-Id: I759d1073b5cb3db5d9df84b35fdceeec0be76e08
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8071337
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Commit-Queue: Amarjot Gill <amarjotgill@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1660217}
---
diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc
index a1bce2a..bec00d17 100644
--- a/services/network/cors/cors_url_loader.cc
+++ b/services/network/cors/cors_url_loader.cc
@@ -707,6 +707,7 @@
if (response_head->response_type != mojom::FetchResponseType::kBasic) {
response_head->device_bound_session_usage =
mojom::DeviceBoundSessionUsage::kUnknown;
+ response_head->did_use_server_http_auth = false;
}
forwarding_client_->OnReceiveResponse(
@@ -785,6 +786,7 @@
response_head->timing_allow_passed = !timing_allow_failed_flag_;
response_head->device_bound_session_usage =
mojom::DeviceBoundSessionUsage::kUnknown;
+ response_head->did_use_server_http_auth = false;
forwarding_client_->OnReceiveRedirect(censored_redirect_info,
std::move(response_head));
return;
@@ -863,6 +865,7 @@
if (response_head->response_type != mojom::FetchResponseType::kBasic) {
response_head->device_bound_session_usage =
mojom::DeviceBoundSessionUsage::kUnknown;
+ response_head->did_use_server_http_auth = false;
}
forwarding_client_->OnReceiveRedirect(redirect_info,
std::move(response_head));
diff --git a/services/network/cors/cors_url_loader_test_util.cc b/services/network/cors/cors_url_loader_test_util.cc
index 4d548cb8..6b7853ef 100644
--- a/services/network/cors/cors_url_loader_test_util.cc
+++ b/services/network/cors/cors_url_loader_test_util.cc
@@ -110,6 +110,13 @@
client_remote_->OnReceiveRedirect(redirect_info, std::move(response));
}
+void TestURLLoaderFactory::NotifyClientOnReceiveRedirect(
+ const net::RedirectInfo& redirect_info,
+ mojom::URLResponseHeadPtr response_head) {
+ DCHECK(client_remote_);
+ client_remote_->OnReceiveRedirect(redirect_info, std::move(response_head));
+}
+
void TestURLLoaderFactory::ResetClientRemote() {
client_remote_.reset();
}
diff --git a/services/network/cors/cors_url_loader_test_util.h b/services/network/cors/cors_url_loader_test_util.h
index 7de0082a..293d2e9 100644
--- a/services/network/cors/cors_url_loader_test_util.h
+++ b/services/network/cors/cors_url_loader_test_util.h
@@ -92,6 +92,9 @@
const net::RedirectInfo& redirect_info,
const std::vector<std::pair<std::string, std::string>>& extra_headers);
+ void NotifyClientOnReceiveRedirect(const net::RedirectInfo& redirect_info,
+ mojom::URLResponseHeadPtr response_head);
+
bool IsCreateLoaderAndStartCalled() { return !!client_remote_; }
void SetOnCreateLoaderAndStart(const base::RepeatingClosure& closure) {
@@ -226,6 +229,14 @@
extra_headers);
}
+ void NotifyLoaderClientOnReceiveRedirect(
+ const net::RedirectInfo& redirect_info,
+ mojom::URLResponseHeadPtr response_head) {
+ DCHECK(test_url_loader_factory_);
+ test_url_loader_factory_->NotifyClientOnReceiveRedirect(
+ redirect_info, std::move(response_head));
+ }
+
void NotifyLoaderClientOnComplete(int error_code) {
DCHECK(test_url_loader_factory_);
test_url_loader_factory_->NotifyClientOnComplete(error_code);
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index a2e36c9..4032e4c 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -573,6 +573,136 @@
client().response_head()->device_bound_session_usage);
}
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthSameOrigin) {
+ const GURL origin("https://example.com");
+ const GURL url("https://example.com/foo.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\nContent-Type: image/png\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveResponse(std::move(response));
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ ASSERT_TRUE(client().has_received_response());
+ EXPECT_EQ(mojom::FetchResponseType::kBasic,
+ client().response_head()->response_type);
+ EXPECT_TRUE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthCrossOriginNoCors) {
+ const GURL origin("https://example.com");
+ const GURL url("https://other.example.com/foo.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\nContent-Type: image/png\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveResponse(std::move(response));
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ ASSERT_TRUE(client().has_received_response());
+ EXPECT_EQ(mojom::FetchResponseType::kOpaque,
+ client().response_head()->response_type);
+ EXPECT_FALSE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthCrossOriginCors) {
+ const GURL origin("https://example.com");
+ const GURL url("https://other.example.com/foo.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\nContent-Type: image/png\n");
+ response->headers->SetHeader("Access-Control-Allow-Origin",
+ "https://example.com");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveResponse(std::move(response));
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ ASSERT_TRUE(client().has_received_response());
+ EXPECT_EQ(mojom::FetchResponseType::kCors,
+ client().response_head()->response_type);
+ EXPECT_FALSE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthSameOriginRedirect) {
+ const GURL origin("https://example.com");
+ const GURL url("https://example.com/foo.png");
+ const GURL new_url("https://example.com/bar.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 301 Moved Permanently\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveRedirect(CreateRedirectInfo(301, "GET", new_url),
+ std::move(response));
+ RunUntilRedirectReceived();
+
+ ASSERT_TRUE(client().has_received_redirect());
+ EXPECT_EQ(mojom::FetchResponseType::kBasic,
+ client().response_head()->response_type);
+ EXPECT_TRUE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthCrossOriginNoCorsRedirect) {
+ const GURL origin("https://example.com");
+ const GURL url("https://other.example.com/foo.png");
+ const GURL new_url("https://other.example.com/bar.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 301 Moved Permanently\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveRedirect(CreateRedirectInfo(301, "GET", new_url),
+ std::move(response));
+ RunUntilRedirectReceived();
+
+ ASSERT_TRUE(client().has_received_redirect());
+ EXPECT_EQ(mojom::FetchResponseType::kOpaque,
+ client().response_head()->response_type);
Regression Test / PoC
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index a2e36c9..4032e4c 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -573,6 +573,136 @@
client().response_head()->device_bound_session_usage);
}
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthSameOrigin) {
+ const GURL origin("https://example.com");
+ const GURL url("https://example.com/foo.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\nContent-Type: image/png\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveResponse(std::move(response));
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ ASSERT_TRUE(client().has_received_response());
+ EXPECT_EQ(mojom::FetchResponseType::kBasic,
+ client().response_head()->response_type);
+ EXPECT_TRUE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthCrossOriginNoCors) {
+ const GURL origin("https://example.com");
+ const GURL url("https://other.example.com/foo.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\nContent-Type: image/png\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveResponse(std::move(response));
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ ASSERT_TRUE(client().has_received_response());
+ EXPECT_EQ(mojom::FetchResponseType::kOpaque,
+ client().response_head()->response_type);
+ EXPECT_FALSE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthCrossOriginCors) {
+ const GURL origin("https://example.com");
+ const GURL url("https://other.example.com/foo.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\nContent-Type: image/png\n");
+ response->headers->SetHeader("Access-Control-Allow-Origin",
+ "https://example.com");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveResponse(std::move(response));
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ ASSERT_TRUE(client().has_received_response());
+ EXPECT_EQ(mojom::FetchResponseType::kCors,
+ client().response_head()->response_type);
+ EXPECT_FALSE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthSameOriginRedirect) {
+ const GURL origin("https://example.com");
+ const GURL url("https://example.com/foo.png");
+ const GURL new_url("https://example.com/bar.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 301 Moved Permanently\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveRedirect(CreateRedirectInfo(301, "GET", new_url),
+ std::move(response));
+ RunUntilRedirectReceived();
+
+ ASSERT_TRUE(client().has_received_redirect());
+ EXPECT_EQ(mojom::FetchResponseType::kBasic,
+ client().response_head()->response_type);
+ EXPECT_TRUE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthCrossOriginNoCorsRedirect) {
+ const GURL origin("https://example.com");
+ const GURL url("https://other.example.com/foo.png");
+ const GURL new_url("https://other.example.com/bar.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNoCors);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 301 Moved Permanently\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveRedirect(CreateRedirectInfo(301, "GET", new_url),
+ std::move(response));
+ RunUntilRedirectReceived();
+
+ ASSERT_TRUE(client().has_received_redirect());
+ EXPECT_EQ(mojom::FetchResponseType::kOpaque,
+ client().response_head()->response_type);
+ EXPECT_FALSE(client().response_head()->did_use_server_http_auth);
+}
+
+TEST_F(CorsURLLoaderTest, DidUseServerHttpAuthManualRedirect) {
+ const GURL origin("https://example.com");
+ const GURL url("https://example.com/foo.png");
+ const GURL new_url("https://example.com/bar.png");
+ CreateLoaderAndStart(origin, url, mojom::RequestMode::kNavigate,
+ mojom::RedirectMode::kManual,
+ mojom::CredentialsMode::kInclude);
+ RunUntilCreateLoaderAndStartCalled();
+
+ auto response = mojom::URLResponseHead::New();
+ response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 301 Moved Permanently\n");
+ response->did_use_server_http_auth = true;
+ NotifyLoaderClientOnReceiveRedirect(CreateRedirectInfo(301, "GET", new_url),
+ std::move(response));
+ RunUntilRedirectReceived();
+
+ ASSERT_TRUE(client().has_received_redirect());
+ EXPECT_EQ(mojom::FetchResponseType::kOpaqueRedirect,
+ client().response_head()->response_type);
+ EXPECT_FALSE(client().response_head()->did_use_server_http_auth);
+}
+
TEST_F(CorsURLLoaderTest,
CrossOriginRequestFetchRequestWithCorsModeButMismatchedCorsHeader) {
const GURL origin("https://example.com");
Original Bug Report
Cross-origin HTTP-auth login state leak to compromised renderer via URLResponseHead
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: A compromised renderer can potentially determine if a user is logged into an HTTP-auth-protected origin and enumerate directory prefixes. This occurs because the did_use_server_http_auth field in URLResponseHead is not sanitized for cross-origin or opaque responses in CorsURLLoader. Because the HTTP auth cache is unpartitioned by default, preemptive authentication attempts will leak this state cross-origin when third-party cookies/state are permitted.
Affected files:
services/network/cors/cors_url_loader.ccservices/network/url_loader_util.ccservices/network/public/mojom/url_response_head.mojom
Estimated timestamp from git blame: 2025-10-01
Root Cause
In Chromium’s network service, url_loader_util::BuildResponseHead() unconditionally copies the network transaction’s did_use_server_http_auth bit into the renderer-bound URLResponseHead structure:
// services/network/url_loader_util.cc:694
response->did_use_server_http_auth = response_info.did_use_server_http_auth;
While sibling fields inside the response head (such as device_bound_session_usage) are explicitly sanitized/cleared for cross-origin (non-basic) responses in CorsURLLoader::OnReceiveResponse and during redirects, did_use_server_http_auth is left untouched:
// services/network/cors/cors_url_loader.cc:703-713
response_head->response_type = response_tainting_;
...
if (response_head->response_type != mojom::FetchResponseType::kBasic) {
response_head->device_bound_session_usage =
mojom::DeviceBoundSessionUsage::kUnknown;
}
// did_use_server_http_auth is NOT cleared
forwarding_client_->OnReceiveResponse(
std::move(response_head), std::move(body), std::move(cached_metadata));
The same lack of sanitization is present on redirect paths in cors_url_loader.cc:786-787 and cors_url_loader.cc:863-866.
Why the Bit Encodes Cross-Origin State
The did_use_server_http_auth bit is set to true whenever the outgoing request carries an Authorization header:
// net/http/http_network_transaction.cc:1433-1434
response_.did_use_server_http_auth =
request_headers_.HasHeader(HttpRequestHeaders::kAuthorization);
When a request is initiated, if credentials already exist in the HttpAuthCache, the browser performs preemptive authentication to avoid an extra 401 challenge round-trip. This is managed by SelectPreemptiveAuth which queries the cache via LookupByPath():
// net/http/http_auth_controller.cc:158-161
HttpAuthCache::Entry* entry = http_auth_cache_->LookupByPath(
auth_scheme_host_port_, target_, network_anonymization_key_, auth_path_);
By default, the HTTP auth cache is unpartitioned because kSplitAuthCacheByNetworkIsolationKey is disabled (services/network/public/cpp/features.cc:92-93). Consequently, credentials entered by the user in a first-party context can be matched and attached to requests initiated by a third-party context (e.g., evil.com).
Potential Attack Scenario
Note: These are potential steps. Our tooling agent does not have the ability to run code or verify exploitability in a live environment.
- The victim previously authenticated to a sensitive HTTP-auth-protected origin (e.g.,
https://victim.intranet/admin/) in a first-party context. - An attacker compromises the renderer process hosting a malicious site (e.g.,
https://evil.com/). - The compromised renderer issues a cross-origin probe request to
https://victim.intranet/admin/xwithcredentials_mode=kIncludeandmode=kNoCors. - If third-party state/cookies are allowed for the target pair, privacy mode is set to
PRIVACY_MODE_DISABLED. The transaction queries the unpartitioned cache and attaches theAuthorizationheader preemptively, settingdid_use_server_http_authtotrue. - The network service returns the response with
did_use_server_http_auth = true.CorsURLLoadermarks the response type askOpaquebut fails to clear this flag. - The compromised renderer reads the
did_use_server_http_authbit from the receivedURLResponseHead(already accessed inthird_party/blink/renderer/platform/exported/resource_load_info_notifier_wrapper.cc:137-138). - By comparing results across different paths, the attacker can deterministically map out the user’s active HTTP-auth logins and directory path prefixes.
Suggested Fix
Sanitize the did_use_server_http_auth field inside CorsURLLoader whenever the response type is not mojom::FetchResponseType::kBasic (similar to how device_bound_session_usage is sanitized). For example:
if (response_head->response_type != mojom::FetchResponseType::kBasic) {
response_head->device_bound_session_usage =
mojom::DeviceBoundSessionUsage::kUnknown;
response_head->did_use_server_http_auth = false;
}
Ensure this sanitization is consistently applied in OnReceiveResponse and redirect handling paths within cors_url_loader.cc.
Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85
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.