Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactSide-channel information leakage in ResourceTiming
DescriptionSide-channel information leakage in ResourceTiming
ComponentResourceTiming
Bug ClassLogic Error
Tracker477180001
Fix commit88392b548909 (chromium/src) +30/-12
CISA KEVNot listed
CreditedPovcfe of Tencent Security Xuanwu Lab
Disclosed2026-03-10

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc
modified

Files Changed

  • third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc
  • third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
  • third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
From 88392b548909e77969f21eeab3249d87e8164509 Mon Sep 17 00:00:00 2001
From: Noam Rosenthal <nrosenthal@chromium.org>
Date: Mon, 02 Feb 2026 03:48:35 -0800
Subject: [PATCH] Clear connection timing when service-worker response is passed to resource timing.

By spec, this information is part of the fetch rather than the response,
and the connection info for a response passed from a service worker is
not the connection info of the client's fetch.

Bug: 477180001
Change-Id: I98145f0976eb8d301a21c20e3c7892a96edd9274
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7516608
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: Yoav Weiss (@Shopify) <yoavweiss@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1578031}
---

diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc b/third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc
index 5685842..eaf288f 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc
@@ -59,14 +59,6 @@
     info->server_timing = ParseServerTimingFromHeaderValueToMojo(
         response->HttpHeaderField(http_names::kServerTiming));
     info->cache_state = response->CacheState();
-    info->alpn_negotiated_protocol = response->AlpnNegotiatedProtocol().IsNull()
-                                         ? g_empty_string
-                                         : response->AlpnNegotiatedProtocol();
-    info->connection_info = response->ConnectionInfoString().IsNull()
-                                ? g_empty_string
-                                : response->ConnectionInfoString();
-
-    info->did_reuse_connection = response->ConnectionReused();
     // Use SecurityOrigin::Create to handle cases like blob:https://.
     info->is_secure_transport = std::ranges::contains(
         url::GetSecureSchemes(),
@@ -74,6 +66,31 @@
     info->timing = response->GetResourceLoadTiming()
                        ? response->GetResourceLoadTiming()->ToMojo()
                        : nullptr;
+
+    if (response->WasFetchedViaServiceWorker()) {
+      // We don't forward connection info to the service worker's client.
+      // This information is available in the service worker's own performance
+      // timeline.
+      // Per-spec, the fetch-timing-info
+      // (https://fetch.spec.whatwg.org/#fetch-timing-info) is associated with
+      // the *fetch* and not attached to a response.
+      if (info->timing) {
+        info->timing->connect_timing =
+            network::mojom::blink::LoadTimingInfoConnectTiming::New();
+      }
+      info->alpn_negotiated_protocol = g_empty_string;
+      info->connection_info = g_empty_string;
+    } else {
+      info->alpn_negotiated_protocol =
+          response->AlpnNegotiatedProtocol().IsNull()
+              ? g_empty_string
+              : response->AlpnNegotiatedProtocol();
+      info->connection_info = response->ConnectionInfoString().IsNull()
+                                  ? g_empty_string
+                                  : response->ConnectionInfoString();
+
+      info->did_reuse_connection = response->ConnectionReused();
+    }
   } else {
     // [spec] https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
 
diff --git a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
index 7a90743..0805db9 100644
--- a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
+++ b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
@@ -31,9 +31,9 @@
   assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`),
                 expected_protocol, 'nextHopProtocol is set on fallback');
   assert_equals(await getNextHopProtocol(frame, `${base_url}`),
-                expected_protocol, 'nextHopProtocol is set on pass-through');
+                '', 'nextHopProtocol is not set on pass-through');
   assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`),
-                expected_protocol, 'nextHopProtocol is set on cached response');
+                '', 'nextHopProtocol is not set on cached response');
 }
 
 promise_test(async (t) => {
diff --git a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
index 2155d7f..ca93a56 100644
--- a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
+++ b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
@@ -29,9 +29,10 @@
       assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.');
       assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.');
       assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.');
-      assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.');
+      assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart is specific to service-workers.');
       assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.');
-      assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.');
+      assert_equals(entry.transferSize, 0, 'transferSize should be 0 in cross-origin request.');
+      assert_equals(entry.nextHopProtocol, "", 'nextHopProtocol should be 0 in cross-origin request.');
       frame.remove();
       await registration.unregister();
   }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
index 7a90743..0805db9 100644
--- a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
+++ b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/next-hop-protocol.https.html
@@ -31,9 +31,9 @@
   assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`),
                 expected_protocol, 'nextHopProtocol is set on fallback');
   assert_equals(await getNextHopProtocol(frame, `${base_url}`),
-                expected_protocol, 'nextHopProtocol is set on pass-through');
+                '', 'nextHopProtocol is not set on pass-through');
   assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`),
-                expected_protocol, 'nextHopProtocol is set on cached response');
+                '', 'nextHopProtocol is not set on cached response');
 }
 
 promise_test(async (t) => {
diff --git a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
index 2155d7f..ca93a56 100644
--- a/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
+++ b/third_party/blink/web_tests/external/wpt/service-workers/service-worker/resource-timing-cross-origin.https.html
@@ -29,9 +29,10 @@
       assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.');
       assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.');
       assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.');
-      assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.');
+      assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart is specific to service-workers.');
       assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.');
-      assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.');
+      assert_equals(entry.transferSize, 0, 'transferSize should be 0 in cross-origin request.');
+      assert_equals(entry.nextHopProtocol, "", 'nextHopProtocol should be 0 in cross-origin request.');
       frame.remove();
       await registration.unregister();
   }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`);
Loading diff…

Original Bug Report

reported by po...@gmail.com

Service Worker subresource responses expose detailed resource timing information for cross-origin resources that are normally restricted, leading to an information leak in the Resource Timing API


Report description

Service Worker subresource responses expose detailed resource timing information for cross-origin resources that are normally restricted, leading to an information leak in the Resource Timing API


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium/src/


The problem

Please describe the technical details of the vulnerability

Chromium exposes detailed network timing and size information for resources via the Resource Timing API (performance.getEntriesByType("resource")). To protect cross-origin privacy, these details are only supposed to be available when the response satisfies the Timing-Allow-Origin (TAO) policy or is same-origin with the calling context.

For subresources intercepted by a Service Worker, the renderer-side subresource loader assumes that constructed responses are always same-origin with the client and unconditionally marks them as passing the timing allow check, regardless of the actual response origin or TAO headers.

In ServiceWorkerSubresourceLoader::StartResponse the URL loader head is filled from the Service Worker FetchAPIResponse, and then timing_allow_passed is set to true for all constructed subresource responses:

// chromium/src/content/renderer/service_worker/service_worker_subresource_loader.cc
void ServiceWorkerSubresourceLoader::StartResponse(
    blink::mojom::FetchAPIResponsePtr response,
    blink::mojom::ServiceWorkerStreamHandlePtr body_as_stream) {
  // ...
  blink::ServiceWorkerLoaderHelpers::SaveResponseInfo(*response,
                                                      response_head_.get());
  response_head_->response_start = base::TimeTicks::Now();
  response_head_->load_timing.receive_headers_start = base::TimeTicks::Now();
  response_head_->load_timing.receive_headers_end =
      response_head_->load_timing.receive_headers_start;
  response_source_ = response->response_source;

  // Constructed subresource responses are always same-origin as the requesting
  // client.
  response_head_->timing_allow_passed = true;
  // ...
}

By contrast, the normal network loader path computes timing_allow_passed based on an explicit timing-allow-origin check:

// chromium/src/services/network/cors/cors_url_loader.cc
// (simplified)
timing_allow_failed_flag_ = !PassesTimingAllowOriginCheck(*response_head);
response_head->timing_allow_passed = !timing_allow_failed_flag_;

On the Blink side, this timing_allow_passed flag directly controls whether detailed timing and connection metadata are exposed to JavaScript. In CreateResourceTimingInfo, if TimingAllowPassed() is true, the allow_timing_details flag is set and the full set of fields are populated:

// chromium/src/third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.cc
mojom::blink::ResourceTimingInfoPtr CreateResourceTimingInfo(
    base::TimeTicks start_time,
    const KURL& initial_url,
    const ResourceResponse* response) {
  mojom::blink::ResourceTimingInfoPtr info =
      mojom::blink::ResourceTimingInfo::New();
  info->start_time = start_time;
  info->name = initial_url;
  info->response_end = base::TimeTicks::Now();
  if (!response) {
    return info;
  }

  if (response->TimingAllowPassed()) {
    info->allow_timing_details = true;
    info->server_timing = ParseServerTimingFromHeaderValueToMojo(
        response->HttpHeaderField(http_names::kServerTiming));
    info->cache_state = response->CacheState();
    info->alpn_negotiated_protocol = response->AlpnNegotiatedProtocol().IsNull()
                                         ? g_empty_string
                                         : response->AlpnNegotiatedProtocol();
    info->connection_info = response->ConnectionInfoString().IsNull()
                                ? g_empty_string
                                : response->ConnectionInfoString();

    info->did_reuse_connection = response->ConnectionReused();
    // Use SecurityOrigin::Create to handle cases like blob:https://.
    info->is_secure_transport = base::Contains(
        url::GetSecureSchemes(),
        SecurityOrigin::Create(response->ResponseUrl())->Protocol().Ascii());
    info->timing = response->GetResourceLoadTiming()
                       ? response->GetResourceLoadTiming()->ToMojo()
                       : nullptr;
  } else {
    // Only limited timing fields are exposed when the timing allow check fails.
    // ...
  }
  // ...
}

ResourceResponse explicitly documents that the response URL may differ from the request URL when a Service Worker responds with a different underlying resource:

// chromium/src/third_party/blink/renderer/platform/loader/fetch/resource_response.h
// ...
// Specifically, if a service worker responded to the request for this
// resource, it may have fetched an entirely different URL and responded with
// that resource. WasFetchedViaServiceWorker() and ResponseUrl() can be used
// to determine whether and how a service worker responded to the request.
// Example service worker code:
//
// onfetch = (event => {
//   if (event.request.url == 'https://abc.com')
//     event.respondWith(fetch('https://def.com'));
// });
//
// If this service worker responds to an "https://abc.com" request, then for
// the resulting ResourceResponse, CurrentRequestUrl() is "https://abc.com",
// WasFetchedViaServiceWorker() is true, and ResponseUrl() is
// "https://def.com".
const KURL& CurrentRequestUrl() const;
void SetCurrentRequestUrl(const KURL&);
// ...
KURL ResponseUrl() const;

The test code under web/fetch_timing/ sets up exactly this pattern:

  • An HTML page served from one origin, for example http://localhost:8080/index.html, registers a Service Worker whose scope covers /proxy.
  • The page offers two actions:
    • Load a resource directly from http://localhost:9000/target.bin via an <img> tag (cross-origin request).
    • Load the same underlying resource via a same-origin <img src="/proxy?...">, where the Service Worker fetches http://localhost:9000/target.bin with mode: "no-cors" and returns the response.
  • After each load, the page inspects performance.getEntriesByType("resource") and logs key fields (responseStart, responseEnd, transferSize, encodedBodySize, decodedBodySize, nextHopProtocol) for the corresponding PerformanceResourceTiming entry.

In this setup:

  • The direct cross-origin load from http://localhost:8080 to http://localhost:9000/target.bin produces a timing entry where sensitive fields are cleared or zeroed, for example:

    • responseStart: 0
    • transferSize: 0
    • encodedBodySize: 0
    • decodedBodySize: 0
    • nextHopProtocol: ""
  • The Service Worker–mediated load uses a same-origin URL such as http://localhost:8080/proxy?... as the observable resource name, but the Service Worker internally fetches the cross-origin target from http://localhost:9000. For this entry, the logged timing data includes detailed values, for example:

    • non-zero responseStart and responseEnd
    • transferSize and nextHopProtocol populated

This demonstrates that code running in the origin that controls the Service Worker can obtain detailed timing and size information about a resource whose actual origin is different and that does not opt in via Timing-Allow-Origin.

Impact analysis

Who can exploit the vulnerability:

  • Any web origin that can register and control a Service Worker for its own pages can exploit this issue for subresource requests it initiates (for example, an attacker-controlled site with a Service Worker whose scope covers a /proxy path).

What they gain when doing so:

  • The attacking origin can obtain detailed timing and connection metadata (such as non-zero responseStart / responseEnd, transferSize, encodedBodySize, decodedBodySize, and nextHopProtocol) for resources that are actually fetched from a different origin and would normally have these fields restricted by the Resource Timing API.
  • This additional information can be used to:
    • perform cross-site performance and availability probing of arbitrary URLs reachable from the browser;
    • infer limited aspects of user or server state from response timing and size patterns (for example, presence of particular resources or cache behavior);
    • strengthen cross-origin device or network fingerprinting by incorporating timing and protocol characteristics of external resources.

The cause

What version of Chrome have you found the security issue in?

145.0.7632.1/stable

No, it is not related to a crash.

Choose the type of vulnerability

Information Leak

How would you like to be publicly acknowledged for your report?

Povcfe of Tencent Security Xuanwu Lab

View on issue tracker