Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Network
DescriptionInformation leak in Network
ComponentNetwork
Bug ClassLogic Error
Tracker513745793
Fix commitfbce76f6c2ed (chromium/src) +105/-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 fbce76f6c2ed4c50a12de9fa96f96f3a9140efc2 Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <[email protected]>
Date: Mon, 29 Jun 2026 16:52:03 -0700
Subject: [PATCH] [DBSC] Clear device_bound_session_usage for non-basic responses

URLResponseHead.device_bound_session_usage is derived solely from the
target URL's SchemefulSite. CorsURLLoader was passing it through to the
client unchanged for opaque and CORS-filtered responses, so a
cross-origin fetch could observe whether the user has a Device Bound
Session at an unrelated site and which paths fall inside its scope.

Reset the field to kUnknown whenever the response type is not kBasic,
both in OnReceiveResponse and in the two OnReceiveRedirect forwarding
paths. Same-origin and navigation requests still surface the real value,
so first-party use-counter coverage is preserved; DevTools reporting via
OnRawRequest is unaffected.

Add CorsURLLoaderTest coverage for the same-origin, opaque, and CORS
cases, plus a NotifyClientOnReceiveResponse overload that takes a
prebuilt URLResponseHead so tests can populate individual response-head
fields.

TAG=agy
CONV=ee22d996-4b11-4e9f-9a82-51d8d28ec963

Bug: 513745793
Change-Id: Iedb76f97ca1d512ce699176145fb7dfdcae362fa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8018442
Reviewed-by: Takashi Toyoshima <[email protected]>
Commit-Queue: Kenichi Ishibashi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1654472}
---

diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc
index 72fb956e..a1bce2a 100644
--- a/services/network/cors/cors_url_loader.cc
+++ b/services/network/cors/cors_url_loader.cc
@@ -44,6 +44,7 @@
 #include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
 #include "services/network/public/cpp/request_mode.h"
 #include "services/network/public/cpp/timing_allow_origin_parser.h"
+#include "services/network/public/mojom/device_bound_sessions.mojom-shared.h"
 #include "services/network/public/mojom/devtools_observer.mojom.h"
 #include "services/network/public/mojom/early_hints.mojom.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
@@ -703,6 +704,10 @@
   response_head->timing_allow_passed = !timing_allow_failed_flag_;
   response_head->has_authorization_covered_by_wildcard_on_preflight =
       has_authorization_covered_by_wildcard_;
+  if (response_head->response_type != mojom::FetchResponseType::kBasic) {
+    response_head->device_bound_session_usage =
+        mojom::DeviceBoundSessionUsage::kUnknown;
+  }
 
   forwarding_client_->OnReceiveResponse(
       std::move(response_head), std::move(body), std::move(cached_metadata));
@@ -778,6 +783,8 @@
         std::make_unique<GURL>(censored_redirect_info.new_url);
     response_head->response_type = mojom::FetchResponseType::kOpaqueRedirect;
     response_head->timing_allow_passed = !timing_allow_failed_flag_;
+    response_head->device_bound_session_usage =
+        mojom::DeviceBoundSessionUsage::kUnknown;
     forwarding_client_->OnReceiveRedirect(censored_redirect_info,
                                           std::move(response_head));
     return;
@@ -853,6 +860,10 @@
     response_head->response_type = response_tainting_;
   }
   response_head->timing_allow_passed = !timing_allow_failed_flag_;
+  if (response_head->response_type != mojom::FetchResponseType::kBasic) {
+    response_head->device_bound_session_usage =
+        mojom::DeviceBoundSessionUsage::kUnknown;
+  }
   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 f52c4fb6..4d548cb8 100644
--- a/services/network/cors/cors_url_loader_test_util.cc
+++ b/services/network/cors/cors_url_loader_test_util.cc
@@ -79,6 +79,14 @@
                                     std::nullopt);
 }
 
+void TestURLLoaderFactory::NotifyClientOnReceiveResponse(
+    mojom::URLResponseHeadPtr response_head,
+    mojo::ScopedDataPipeConsumerHandle body) {
+  DCHECK(client_remote_);
+  client_remote_->OnReceiveResponse(std::move(response_head), std::move(body),
+                                    std::nullopt);
+}
+
 void TestURLLoaderFactory::NotifyClientOnComplete(int error_code) {
   DCHECK(client_remote_);
   client_remote_->OnComplete(URLLoaderCompletionStatus(error_code));
diff --git a/services/network/cors/cors_url_loader_test_util.h b/services/network/cors/cors_url_loader_test_util.h
index e135228b..7de0082a 100644
--- a/services/network/cors/cors_url_loader_test_util.h
+++ b/services/network/cors/cors_url_loader_test_util.h
@@ -31,6 +31,7 @@
 #include "services/network/public/mojom/network_context.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
 #include "services/network/resource_scheduler/resource_scheduler.h"
 #include "services/network/test/test_url_loader_network_observer.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -80,6 +81,9 @@
       const std::vector<std::pair<std::string, std::string>>& extra_headers,
       mojo::ScopedDataPipeConsumerHandle body);
 
+  void NotifyClientOnReceiveResponse(mojom::URLResponseHeadPtr response_head,
+                                     mojo::ScopedDataPipeConsumerHandle body);
+
   void NotifyClientOnComplete(int error_code);
 
   void NotifyClientOnComplete(const CorsErrorStatus& status);
@@ -204,6 +208,15 @@
         status_code, extra_headers, std::move(body));
   }
 
+  void NotifyLoaderClientOnReceiveResponse(
+      mojom::URLResponseHeadPtr response_head,
+      mojo::ScopedDataPipeConsumerHandle body =
+          mojo::ScopedDataPipeConsumerHandle()) {
+    DCHECK(test_url_loader_factory_);
+    test_url_loader_factory_->NotifyClientOnReceiveResponse(
+        std::move(response_head), std::move(body));
+  }
+
   void NotifyLoaderClientOnReceiveRedirect(
       const net::RedirectInfo& redirect_info,
       const std::vector<std::pair<std::string, std::string>>& extra_headers =
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index e007dfe5..a2e36c9 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -19,6 +19,7 @@
 #include "net/cookies/cookie_util.h"
 #include "net/cookies/site_for_cookies.h"
 #include "net/http/http_request_headers.h"
+#include "net/http/http_response_headers.h"
 #include "net/log/test_net_log_util.h"
 #include "net/storage_access_api/status.h"
 #include "net/test/gtest_util.h"
@@ -29,6 +30,7 @@
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/cors.mojom.h"
+#include "services/network/public/mojom/device_bound_sessions.mojom-shared.h"
 #include "services/network/public/mojom/network_context.mojom.h"
 #include "services/network/public/mojom/url_request.mojom-forward.h"
 #include "services/network/test/mock_devtools_observer.h"
@@ -500,6 +502,77 @@
   EXPECT_EQ(net::OK, client().completion_status().error_code);
 }
 
+TEST_F(CorsURLLoaderTest, DeviceBoundSessionUsageSameOrigin) {
+  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->device_bound_session_usage =
+      mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded;
+  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_EQ(mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded,
+            client().response_head()->device_bound_session_usage);
+}
+
+TEST_F(CorsURLLoaderTest, DeviceBoundSessionUsageCrossOriginNoCors) {
+  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->device_bound_session_usage =
+      mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded;
+  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_EQ(mojom::DeviceBoundSessionUsage::kUnknown,
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 e007dfe5..a2e36c9 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -19,6 +19,7 @@
 #include "net/cookies/cookie_util.h"
 #include "net/cookies/site_for_cookies.h"
 #include "net/http/http_request_headers.h"
+#include "net/http/http_response_headers.h"
 #include "net/log/test_net_log_util.h"
 #include "net/storage_access_api/status.h"
 #include "net/test/gtest_util.h"
@@ -29,6 +30,7 @@
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/cors.mojom.h"
+#include "services/network/public/mojom/device_bound_sessions.mojom-shared.h"
 #include "services/network/public/mojom/network_context.mojom.h"
 #include "services/network/public/mojom/url_request.mojom-forward.h"
 #include "services/network/test/mock_devtools_observer.h"
@@ -500,6 +502,77 @@
   EXPECT_EQ(net::OK, client().completion_status().error_code);
 }
 
+TEST_F(CorsURLLoaderTest, DeviceBoundSessionUsageSameOrigin) {
+  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->device_bound_session_usage =
+      mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded;
+  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_EQ(mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded,
+            client().response_head()->device_bound_session_usage);
+}
+
+TEST_F(CorsURLLoaderTest, DeviceBoundSessionUsageCrossOriginNoCors) {
+  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->device_bound_session_usage =
+      mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded;
+  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_EQ(mojom::DeviceBoundSessionUsage::kUnknown,
+            client().response_head()->device_bound_session_usage);
+}
+
+TEST_F(CorsURLLoaderTest, DeviceBoundSessionUsageCrossOriginCors) {
+  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->device_bound_session_usage =
+      mojom::DeviceBoundSessionUsage::kInScopeRefreshNotYetNeeded;
+  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_EQ(mojom::DeviceBoundSessionUsage::kUnknown,
+            client().response_head()->device_bound_session_usage);
+}
+
 TEST_F(CorsURLLoaderTest,
        CrossOriginRequestFetchRequestWithCorsModeButMismatchedCorsHeader) {
   const GURL origin("https://example.com");
Loading diff…

Original Bug Report

reported by [email protected]

Cross-origin login state leak via DBSC session usage in 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: The device_bound_session_usage field in the Network Service’s response metadata potentially leaks the presence and scope of Device Bound Session Credentials (DBSC) for any site. A compromised renderer can use this as a cross-origin login oracle to determine if a user is signed into a specific site, bypassing Site Isolation and third-party cookie blocking.

Affected files:

  • services/network/url_loader_util.cc
  • net/device_bound_sessions/session_service_impl.cc
  • net/device_bound_sessions/session.cc
  • net/url_request/url_request_http_job.cc
  • services/network/public/mojom/url_response_head.mojom
  • services/network/cors/cors_url_loader.cc
  • services/network/orb/orb_api.cc

Estimated timestamp from git blame: 2025-04-16

Technical Description

A potential information leak exists in the implementation of Device Bound Session Credentials (DBSC) within the Chromium Network Service. The URLResponseHead.device_bound_session_usage field is populated with the DBSC status of a request and delivered to the renderer process without adequate origin checks or sanitization during security policy enforcement (CORS/ORB).

Root Cause

In services/network/url_loader_util.cc, the function BuildResponseHead() unconditionally populates the device_bound_session_usage field based on the state recorded on the net::URLRequest during the DBSC check path:

// services/network/url_loader_util.cc
response->device_bound_session_usage =
    static_cast<network::mojom::DeviceBoundSessionUsage>(
        net::device_bound_sessions::GetMaxUsage(
            url_request.device_bound_session_usage()));

The usage state is determined in net/device_bound_sessions/session_service_impl.cc and net/device_bound_sessions/session.cc. The logic evaluates whether a DBSC session exists for the target origin’s site, whether the specific request path is in scope, and whether the request was deferred (e.g., due to missing bound cookies).

Critically, Opaque Response Blocking (ORB) and CORS do not sanitize this field. In services/network/orb/orb_api.cc, the SanitizeBlockedResponseHeaders function strips standard HTTP headers and zeros the content length but leaves device_bound_session_usage intact. This allows the field to reach a compromised renderer even for cross-origin requests where the body and headers are blocked.

Potential Exploitation Steps

Note: These steps are based on source code analysis and have not been validated with a functional proof of concept.

  1. An attacker achieves code execution in a sandboxed renderer process (e.g., via a memory corruption bug).
  2. The compromised renderer issues a ResourceRequest to a victim origin (e.g., https://accounts.google.com/) with mode=kNoCors and credentials_mode=kInclude via the URLLoaderFactory Mojo interface.
  3. The Network Service processes the request. The DBSC logic updates the request’s internal session usage map based on existing sessions for that site.
  4. Upon receiving the server response, the Network Service populates URLResponseHead.device_bound_session_usage. If a DBSC session exists for the victim origin, the value will be non-zero (e.g., kSiteMatchNotInScope, kInScope*, or kDeferred).
  5. The response metadata is delivered to the renderer’s URLLoaderClient::OnReceiveResponse callback.
  6. The renderer inspects the field. A value > 1 serves as a high-fidelity signal that the user is currently logged into the target origin via DBSC. By probing different paths, the attacker can further deduce the session’s scope rules.

Impact

This issue constitutes a cross-origin information leak that allows a compromised renderer to determine user login status for unrelated origins. It bypasses Site Isolation’s data confidentiality guarantees and functions even when third-party cookies are blocked, as DBSC checks occur independently of standard cookie inclusion rules. The DBSC feature is currently enabled by default on Windows.

Suggested Fix

The device_bound_session_usage field should be sanitized or cleared in network::orb::SanitizeBlockedResponseHeaders and potentially in network::cors::CorsURLLoader::OnReceiveResponse if the response is opaque or if the initiator is not same-origin with the target. Metadata describing the internal DBSC state of a cross-origin request should not be exposed to the renderer unless it is same-origin with the target.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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
Links in the report