Medium watch CVSS 3.1 chrome Cross Origin 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in CORS
DescriptionInformation leak in CORS
ComponentCORS
Bug ClassCross Origin
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker532957785
Fix commit9be50510944a (chromium/src) +151/-0
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-07-10 (public git)
Disclosed2026-08-25
Patch gap46 days — fix public in source before the advisory

Background

`CorsURLLoader`
the network-service loader that enforces CORS and classifies each response with a mojom::FetchResponseType (kBasic, kCors, kOpaque, kOpaqueRedirect) before forwarding it to the requesting (typically renderer) client.
`URLResponseHead`
the Mojo struct of response metadata sent to the client; fields that reveal cross-origin state must be scrubbed for non-kBasic responses.
`did_use_server_http_auth`
a URLResponseHead field indicating that server HTTP authentication (e.g. Basic/Digest credentials) was used for the response.
Response sanitisation
CorsURLLoader already reset device_bound_session_usage to mojom::DeviceBoundSessionUsage::kUnknown for non-basic responses, establishing the pattern of censoring cross-origin metadata.

Root Cause Analysis

When CorsURLLoader forwarded a response or redirect to forwarding_client_, it censored cross-origin metadata for any response whose response_type was not mojom::FetchResponseType::kBasic, but only reset device_bound_session_usage; did_use_server_http_auth was passed through unchanged.

As a result, an opaque (no-cors cross-origin), CORS, or opaque-redirect response still told the requesting process whether the cross-origin server’s response involved HTTP authentication, violating the invariant that only same-origin (kBasic) responses expose such credential state.

The fix adds response_head->did_use_server_http_auth = false; at three forwarding points: before forwarding_client_->OnReceiveResponse, in the manual-redirect path that forwards a censored_redirect_info, and before the general forwarding_client_->OnReceiveRedirect. The hunk headers do not name the enclosing functions; from the forwarding calls they are the response- and redirect-handling paths of CorsURLLoader (presumably CorsURLLoader::OnReceiveResponse and CorsURLLoader::OnReceiveRedirect). The added unit tests confirm the field survives only for kBasic responses/redirects and is cleared for kOpaque, kCors and kOpaqueRedirect.

Key insight
A newer credential-state field on URLResponseHead was not added to the existing cross-origin censoring next to device_bound_session_usage, so it leaked through opaque/CORS responses; the fix clears it whenever the response type is not kBasic.

Attack Path

  1. Cross-origin request An attacker page issues a request to a victim origin, e.g. a no-cors subresource load, a CORS fetch, or a navigation-style request with manual redirect mode (the shapes used in the regression tests).
  2. Credentialed response If the user has HTTP-auth credentials cached for the victim origin, the network stack reports did_use_server_http_auth = true on the response or redirect.
  3. Unsanitised forwarding Pre-patch, CorsURLLoader forwarded that value to the requesting client even though the response was kOpaque, kCors or kOpaqueRedirect.
  4. Cross-origin inference Code in the requesting renderer learns whether the user is authenticated to the victim site; whether this value is observable from ordinary web JavaScript or only from a compromised renderer is not shown by the patch (inferred).

Impact Assessment

The leak is cross-origin information: whether a third-party server used HTTP authentication for the user, i.e. a signal about the user’s login/credential state with another origin, exposed to the requesting renderer process. Ordinary web pages can start the requests, but turning the field into an observable signal likely needs either a renderer-side consumer that surfaces it or a compromised renderer that reads URLResponseHead directly (inferred). The advisory describes it only as “Information leak in CORS” and does not state in-the-wild exploitation.

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_test_util.h
  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_unittest.cc
  • services/network/cors/cors_url_loader_test_util.cc

Audit Directions

  • New `URLResponseHead` fields
    For every field added to URLResponseHead, check whether it carries cross-origin or credential state and, if so, whether CorsURLLoader (and any other sanitiser) censors it for kOpaque, kCors and kOpaqueRedirect responses; prefer an allowlist-style sanitiser over per-field resets.
  • Multiple forwarding points
    Ensure sanitisation is applied consistently on every path that forwards a response or redirect (normal response, manual redirect, follow redirect, service-worker and preload paths), not only the most common one.
  • Credential-state side channels
    Look for other indicators of auth state (cookie usage, device-bound sessions, client certificates, Authorization reuse) that might reach renderers for cross-origin loads.
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);
+  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…

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