Medium chrome Cross Origin 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in CORS
DescriptionInformation leak in CORS
ComponentCORS
Bug ClassCross Origin
Tracker532957785
Fix commit9be50510944a (chromium/src) +151/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
services/network/cors/cors_url_loader.cc
modified
TEST_F
services/network/cors/cors_url_loader_unittest.cc
modified

Files Changed

  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_test_util.cc
  • services/network/cors/cors_url_loader_test_util.h
  • services/network/cors/cors_url_loader_unittest.cc
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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");
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • services/network/url_loader_util.cc
  • services/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.

  1. The victim previously authenticated to a sensitive HTTP-auth-protected origin (e.g., https://victim.intranet/admin/) in a first-party context.
  2. An attacker compromises the renderer process hosting a malicious site (e.g., https://evil.com/).
  3. The compromised renderer issues a cross-origin probe request to https://victim.intranet/admin/x with credentials_mode=kInclude and mode=kNoCors.
  4. 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 the Authorization header preemptively, setting did_use_server_http_auth to true.
  5. The network service returns the response with did_use_server_http_auth = true. CorsURLLoader marks the response type as kOpaque but fails to clear this flag.
  6. The compromised renderer reads the did_use_server_http_auth bit from the received URLResponseHead (already accessed in third_party/blink/renderer/platform/exported/resource_load_info_notifier_wrapper.cc:137-138).
  7. 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.

View on issue tracker