Medium watch ✔︎ verified CVSS 5.3 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Network
DescriptionInformation leak in Network
ComponentNetwork
Sub-componentDBSC
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — upheld by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Impact wrongly says the advisory gives no severity; it states medium, CVSS 5.3. Commit message says a cross-origin fetch 'could observe' the state, so hedging that a compromised renderer is most plausibly needed is unsupported.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker513745793
Fix commitfbce76f6c2ed (chromium/src) +105/-0
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-06-29 (public git)
Disclosed2026-08-25
Patch gap57 days — fix public in source before the advisory

Background

Device Bound Session Credentials (DBSC)
a mechanism that binds a site’s session to a device key. The network service tracks the user’s sessions per site and their URL scope.
`device_bound_session_usage`
a URLResponseHead field of type mojom::DeviceBoundSessionUsage, such as kInScopeRefreshNotYetNeeded or kUnknown. According to the commit message, it is derived solely from the target URL’s SchemefulSite and records whether the request fell inside a DBSC session’s scope.
`CorsURLLoader`
the network-service loader that applies CORS and response tainting, then forwards the response head to the requesting client (usually a renderer) through forwarding_client_.
Response type / tainting
mojom::FetchResponseType values kBasic (same-origin), kCors, kOpaque and kOpaqueRedirect. Non-basic responses are filtered, so cross-origin metadata must not reach the requester.

Root Cause Analysis

CorsURLLoader filled in CORS-related response-head fields such as response_type and timing_allow_passed before forwarding responses and redirects. It passed device_bound_session_usage through unchanged, even when the response was cross-origin (kOpaque, kCors, kOpaqueRedirect). That field describes the user’s DBSC state at the target site. For a cross-origin request it therefore exposed whether the user has a device-bound session at an unrelated site, and which paths fall inside that session’s scope, to the requesting origin’s process. This breaks the rule that filtered (non-basic) responses carry no cross-origin user state beyond what Fetch allows.

The fix resets the field to kUnknown in three places: in the response path whenever response_type != kBasic, in the manual-redirect path that produces kOpaqueRedirect, and in the general redirect-forwarding path whenever the tainted response_type is not kBasic. Same-origin responses keep the real value, as the DeviceBoundSessionUsageSameOrigin test checks.

Key insight
A per-site, user-state field was copied onto cross-origin response heads without the filtering applied to other cross-origin metadata. The fix replaces the value with kUnknown on every non-kBasic response and redirect that CorsURLLoader forwards.

Attack Path

  1. Victim has a DBSC session the user has a device-bound session at some site, for example other.example.com. This requires DBSC to be enabled and in use by that site.
  2. Cross-origin request a page on an attacker origin issues a no-cors or cors request (an image load or fetch) to URLs on that site, as the DeviceBoundSessionUsageCrossOriginNoCors and DeviceBoundSessionUsageCrossOriginCors tests do.
  3. Field forwarded unfiltered before the fix, CorsURLLoader forwarded device_bound_session_usage with the real value, such as kInScopeRefreshNotYetNeeded, to the attacker’s renderer on opaque/CORS responses and redirects.
  4. State inference by comparing the value across many URLs, the attacker learns whether a session exists and the shape of its scope. The patch shows only the data reaching the renderer. Whether script can read it without a compromised renderer, for example through use-counter or timing side effects, is (inferred).

Impact Assessment

The bug is a cross-site information leak in the network service: the requesting renderer receives another site’s DBSC session state and scope membership. That reveals whether the user is logged in at the target site, along with session-scope details, which goes against site-isolation expectations. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 5.3. DBSC must be enabled and in use by the target site. Reading the field directly most plausibly requires a compromised renderer, and any web-observable channel is (inferred).

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

Audit Directions

  • New `URLResponseHead` fields vs. CORS filtering
    check every recently added response-head field that reflects user, session or cookie state (DBSC, storage access, privacy-sandbox signals) to see whether CorsURLLoader or Blink’s response filtering clears it for kOpaque, kCors and kOpaqueRedirect responses.
  • Redirect paths parity
    in loaders and throttles, check that sanitization applied in OnReceiveResponse is repeated in every OnReceiveRedirect branch, including manual-redirect and censored-redirect paths.
  • Other cross-origin forwarding layers
    review service-worker, prefetch, and navigation loaders that build or forward URLResponseHead without going through CorsURLLoader, to confirm the same per-site state is scrubbed there too.
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,
+            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…

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