Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in ServiceWorker
DescriptionInformation leak in ServiceWorker
ComponentServiceWorker
Bug ClassLogic Error
Tracker517415433
Fix commitdd02a2a30868 (chromium/src) +121/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Background

RaceNetworkRequest
A ServiceWorker optimization that dispatches a network request in parallel with the fetch handler so the faster of the two can satisfy a navigation.
`URLResponseHead`
The Mojo struct carrying a response’s metadata (headers plus optional fields like ssl_info and load_timing_internal_info) from the network process to a renderer client.
`kURLLoadOptionSendSSLInfoWithResponse`
A load flag that tells the network process to attach ssl_info to the URLResponseHead, normally used only for trusted navigation clients.
Fetch handler client
The ServiceWorker-side URLLoaderClient that observes the raced response as an ordinary subresource fetch and does not need navigation-privileged metadata.

Root Cause Analysis

Because a main-resource navigation issues its race-network request with kURLLoadOptionSendSSLInfoWithResponse and trusted_params, the network process populates the URLResponseHead with ssl_info and load_timing_internal_info intended solely for the navigation client (owner_). ServiceWorkerRaceNetworkRequestURLLoaderClient then forwarded that same head verbatim to the fetch-handler client via forwarding_client_, so the ServiceWorker script observed navigation-only, trusted metadata on what it should see as a regular subresource fetch. The violated invariant is that a subresource-fetch view of a response must not expose the elevated fields reserved for a trusted navigation consumer.

The fix adds SanitizeResponseHeadForFetchHandler, which resets load_timing_internal_info and ssl_info on the head before every forward to forwarding_client_ in OnReceiveResponse and both OnReceiveRedirect paths, while leaving the head committed to owner_ untouched. This works because the sensitive fields are stripped precisely on the boundary crossing into the less-privileged fetch-handler channel.

Key insight
The single mistake was reusing one URLResponseHead — provisioned with navigation-privileged ssl_info and load_timing_internal_info — for both the trusted navigation client and the untrusted fetch-handler client; the fix scrubs those two fields on the copy forwarded to the fetch handler, so only the navigation consumer retains them.

Attack Path

  1. Register a ServiceWorker with a fetch handler An attacker-controlled origin installs a ServiceWorker whose fetch handler inspects the Response objects it receives.
  2. Trigger a race-network navigation The victim navigates to a page in that scope, causing RaceNetworkRequest to issue the main-resource request with kURLLoadOptionSendSSLInfoWithResponse and trusted_params.
  3. Receive the unsanitized head The network process attaches ssl_info and load_timing_internal_info to the URLResponseHead, which is forwarded unchanged to the fetch handler via forwarding_client_->OnReceiveResponse.
  4. Read navigation-only metadata The fetch handler observes SSL and internal timing details that a normal subresource fetch would never carry, leaking them into the ServiceWorker script context.

Impact Assessment

An attacker running a ServiceWorker fetch handler gains read access to navigation-only response metadata (ssl_info and internal load timing) that is normally confined to the trusted navigation client, all within the renderer/ServiceWorker script context. The precondition is that the victim performs a race-eligible main-resource navigation into a scope controlled by an attacker-registered ServiceWorker. Consistent with the medium-severity LogicError classification, this is an information leak rather than memory corruption or a control-plane compromise.

Changed Functions

FunctionChangeNotes
if
content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
modified
if
content/common/service_worker/race_network_request_url_loader_client.cc
modified
if
content/common/service_worker/race_network_request_url_loader_client_unittest.cc
modified

Files Changed

  • content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
  • content/common/service_worker/race_network_request_url_loader_client.cc
  • content/common/service_worker/race_network_request_url_loader_client_unittest.cc

Audit Directions

  • Trusted-field reuse across clients
    Audit every path where one URLResponseHead provisioned for a trusted_params/navigation client is also forwarded to a less-privileged consumer, and confirm ssl_info and load_timing_internal_info are reset at the boundary.
  • RaceNetworkRequest forwarding boundaries
    Review other forwarding_client_ calls and pipe-fusing paths in the ServiceWorker RaceNetworkRequest code for metadata that should be scoped to owner_ only.
  • Load-option propagation
    Trace consumers of kURLLoadOptionSendSSLInfoWithResponse to verify SSL and internal-timing data attached for one privileged consumer is never surfaced to script-observable subresource fetches.
From dd02a2a308680efeb6d498bc5e4f3399123f90ef Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <chikamune@chromium.org>
Date: Wed, 29 Jul 2026 00:34:57 -0700
Subject: [PATCH] [ServiceWorker] RaceNetworkRequest: drop navigation-only response head fields before forwarding to the fetch handler

The race-network request for a main-resource navigation is issued with
kURLLoadOptionSendSSLInfoWithResponse and trusted_params, so the network
process attaches ssl_info and load_timing_internal_info to the
URLResponseHead for the navigation client. The fetch-handler client only
observes the response as a regular subresource fetch and does not need
either field, so reset them in
ServiceWorkerRaceNetworkRequestURLLoaderClient before forwarding to
forwarding_client_ (OnReceiveResponse and OnReceiveRedirect). The head
committed to the navigation client (owner_) is unchanged.

Also update the FusePipes comment in
ForwardedRaceNetworkRequestURLLoaderFactory to record this
responsibility.

TAG=agy
CONV=a8009315-be67-4ed0-92d9-1a4e1b5557c2

Fixed: 517415433
Change-Id: Iaf75ce18a63618854a9650e4722d05a3e3f809da
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8161402
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Commit-Queue: Minoru Chikamune <chikamune@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1670036}
---

diff --git a/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc b/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
index f283bd7..ecc4bf0f2 100644
--- a/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
+++ b/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
@@ -86,10 +86,12 @@
       is_data_pipe_fused_);
   if (!is_data_pipe_fused_) {
     // We fuse the URLLoaderClient pipes directly. The URLLoaderClient is a data
-    // delivery channel (outgoing from the network process to the renderer). It
-    // only carries response data and metadata (like the Mojo Data Pipe handle
-    // for the body), and does not expose any control methods that the renderer
-    // can use to drive the network process.
+    // delivery channel (outgoing from the network process to the renderer) and
+    // does not expose any control methods that the renderer can use to drive
+    // the network process. Note that
+    // ServiceWorkerRaceNetworkRequestURLLoaderClient omits navigation-only
+    // response head fields (such as load_timing_internal_info and ssl_info)
+    // before forwarding responses or redirects over this channel.
     bool result =
         mojo::FusePipes(std::move(client_receiver_), std::move(client));
     CHECK(result) << resource_request.url;
diff --git a/content/common/service_worker/race_network_request_url_loader_client.cc b/content/common/service_worker/race_network_request_url_loader_client.cc
index ecd9965..9a07436 100644
--- a/content/common/service_worker/race_network_request_url_loader_client.cc
+++ b/content/common/service_worker/race_network_request_url_loader_client.cc
@@ -70,6 +70,19 @@
   kMaxValue = kBothNotCompleted
 };
 // LINT.ThenChange(//tools/metrics/histograms/metadata/service/enums.xml:RaceNetworkRequestDataTransferResult)
+
+// Omit navigation-only fields before forwarding the response head to the fetch
+// handler. The fetch handler observes the response as a subresource fetch and
+// does not require internal timing or SSL info.
+void SanitizeResponseHeadForFetchHandler(
+    network::mojom::URLResponseHeadPtr& head) {
+  if (!head) {
+    return;
+  }
+  head->load_timing_internal_info.reset();
+  head->ssl_info.reset();
+}
+
 }  // namespace
 
 ServiceWorkerRaceNetworkRequestURLLoaderClient::
@@ -249,6 +262,7 @@
     case FetchResponseFrom::kSubresourceLoaderIsHandlingRedirect:
       // This happens when the response is faster than the fetch handler.
       owner_->SetCommitResponsibility(FetchResponseFrom::kServiceWorker);
+      SanitizeResponseHeadForFetchHandler(head);
       forwarding_client_->OnReceiveRedirect(forwarding_redirect_info,
                                             std::move(head));
       MaybeCompleteRedirectResponse(/*run_completion_callback=*/false);
@@ -259,6 +273,7 @@
       // handler is already executed but in rare case in-flight request may be
       // used. Let the fetch handler side client to handle the rest. The fetch
       // handler side close the connection if it's not needed anyway.
+      SanitizeResponseHeadForFetchHandler(head);
       forwarding_client_->OnReceiveRedirect(forwarding_redirect_info,
                                             std::move(head));
       MaybeCompleteRedirectResponse(/*run_completion_callback=*/true);
@@ -763,6 +778,7 @@
   // debug crbug.com/463388771.
   CHECK(!has_forwarded_response_);
   has_forwarded_response_ = true;
+  SanitizeResponseHeadForFetchHandler(head);
   forwarding_client_->OnReceiveResponse(std::move(head), std::move(body),
                                         std::move(cached_metadata));
 }
diff --git a/content/common/service_worker/race_network_request_url_loader_client_unittest.cc b/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
index b93b89f..abc475a 100644
--- a/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
+++ b/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
@@ -15,10 +15,14 @@
 #include "content/common/service_worker/race_network_request_write_buffer_manager.h"
 #include "mojo/public/cpp/system/data_pipe.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
+#include "net/base/load_timing_internal_info.h"
 #include "net/base/net_errors.h"
+#include "net/ssl/ssl_info.h"
 #include "net/url_request/redirect_info.h"
 #include "services/network/public/cpp/loading_params.h"
 #include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/load_timing_internal_info.mojom.h"
+#include "services/network/public/mojom/ssl_info.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/test/test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -70,7 +74,11 @@
       const network::mojom::URLResponseHeadPtr& response_head,
       mojo::ScopedDataPipeConsumerHandle response_body,
       std::optional<mojo_base::BigBuffer> cached_metadata) override {
-    std::move(on_commit_response_).Run(response_head, std::move(response_body));
+    committed_response_head_ = response_head->Clone();
+    if (on_commit_response_) {
+      std::move(on_commit_response_)
+          .Run(response_head, std::move(response_body));
+    }
   }
   void CommitEmptyResponseAndComplete() override {}
   void CommitCompleted(int error_code, const char* reason) override {
@@ -86,6 +94,10 @@
     return received_redirect_info_;
   }
 
+  const network::mojom::URLResponseHeadPtr& committed_response_head() const {
+    return committed_response_head_;
+  }
+
   base::WeakPtr<MockServiceWorkerResourceLoader> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
   }
@@ -99,6 +111,7 @@
   }
 
  private:
+  network::mojom::URLResponseHeadPtr committed_response_head_;
   OnCommitResponseCallback on_commit_response_;
   OnCompletedCallback on_commit_completed_;
   std::optional<net::RedirectInfo> received_redirect_info_;
@@ -209,11 +222,16 @@
       network::mojom::URLResponseHeadPtr head,
       mojo::ScopedDataPipeConsumerHandle body,
       std::optional<mojo_base::BigBuffer> cached_metadata) override {
+    received_response_head_ = head->Clone();
+    if (on_receive_response_callback_) {
+      std::move(on_receive_response_callback_).Run();
+    }
     WatchResponseBody(head, std::move(body));
   }
   void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
                          network::mojom::URLResponseHeadPtr head) override {
     received_redirect_info_ = redirect_info;
+    received_redirect_head_ = head->Clone();
     if (on_receive_redirect_callback_) {
       std::move(on_receive_redirect_callback_).Run();
     }
@@ -224,6 +242,10 @@
   void OnTransferSizeUpdated(int32_t transfer_size_diff) override {}
   void OnComplete(const network::URLLoaderCompletionStatus& status) override {}
 
+  void SetOnReceiveResponseCallback(base::OnceClosure callback) {
+    on_receive_response_callback_ = std::move(callback);
+  }
+
   void SetOnReceiveRedirectCallback(base::OnceClosure callback) {
     on_receive_redirect_callback_ = std::move(callback);
   }
@@ -232,9 +254,20 @@
     return received_redirect_info_;
   }
 
+  const network::mojom::URLResponseHeadPtr& received_response_head() const {
+    return received_response_head_;
+  }
+
+  const network::mojom::URLResponseHeadPtr& received_redirect_head() const {
+    return received_redirect_head_;
+  }
+
  private:
   mojo::Receiver<network::mojom::URLLoaderClient> receiver_{this};
   std::optional<net::RedirectInfo> received_redirect_info_;
+  network::mojom::URLResponseHeadPtr received_response_head_;
+  network::mojom::URLResponseHeadPtr received_redirect_head_;
+  base::OnceClosure on_receive_response_callback_;
   base::OnceClosure on_receive_redirect_callback_;
 };
 
@@ -281,6 +314,10 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/common/service_worker/race_network_request_url_loader_client_unittest.cc b/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
index b93b89f..abc475a 100644
--- a/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
+++ b/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
@@ -15,10 +15,14 @@
 #include "content/common/service_worker/race_network_request_write_buffer_manager.h"
 #include "mojo/public/cpp/system/data_pipe.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
+#include "net/base/load_timing_internal_info.h"
 #include "net/base/net_errors.h"
+#include "net/ssl/ssl_info.h"
 #include "net/url_request/redirect_info.h"
 #include "services/network/public/cpp/loading_params.h"
 #include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/load_timing_internal_info.mojom.h"
+#include "services/network/public/mojom/ssl_info.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/test/test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -70,7 +74,11 @@
       const network::mojom::URLResponseHeadPtr& response_head,
       mojo::ScopedDataPipeConsumerHandle response_body,
       std::optional<mojo_base::BigBuffer> cached_metadata) override {
-    std::move(on_commit_response_).Run(response_head, std::move(response_body));
+    committed_response_head_ = response_head->Clone();
+    if (on_commit_response_) {
+      std::move(on_commit_response_)
+          .Run(response_head, std::move(response_body));
+    }
   }
   void CommitEmptyResponseAndComplete() override {}
   void CommitCompleted(int error_code, const char* reason) override {
@@ -86,6 +94,10 @@
     return received_redirect_info_;
   }
 
+  const network::mojom::URLResponseHeadPtr& committed_response_head() const {
+    return committed_response_head_;
+  }
+
   base::WeakPtr<MockServiceWorkerResourceLoader> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
   }
@@ -99,6 +111,7 @@
   }
 
  private:
+  network::mojom::URLResponseHeadPtr committed_response_head_;
   OnCommitResponseCallback on_commit_response_;
   OnCompletedCallback on_commit_completed_;
   std::optional<net::RedirectInfo> received_redirect_info_;
@@ -209,11 +222,16 @@
       network::mojom::URLResponseHeadPtr head,
       mojo::ScopedDataPipeConsumerHandle body,
       std::optional<mojo_base::BigBuffer> cached_metadata) override {
+    received_response_head_ = head->Clone();
+    if (on_receive_response_callback_) {
+      std::move(on_receive_response_callback_).Run();
+    }
     WatchResponseBody(head, std::move(body));
   }
   void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
                          network::mojom::URLResponseHeadPtr head) override {
     received_redirect_info_ = redirect_info;
+    received_redirect_head_ = head->Clone();
     if (on_receive_redirect_callback_) {
       std::move(on_receive_redirect_callback_).Run();
     }
@@ -224,6 +242,10 @@
   void OnTransferSizeUpdated(int32_t transfer_size_diff) override {}
   void OnComplete(const network::URLLoaderCompletionStatus& status) override {}
 
+  void SetOnReceiveResponseCallback(base::OnceClosure callback) {
+    on_receive_response_callback_ = std::move(callback);
+  }
+
   void SetOnReceiveRedirectCallback(base::OnceClosure callback) {
     on_receive_redirect_callback_ = std::move(callback);
   }
@@ -232,9 +254,20 @@
     return received_redirect_info_;
   }
 
+  const network::mojom::URLResponseHeadPtr& received_response_head() const {
+    return received_response_head_;
+  }
+
+  const network::mojom::URLResponseHeadPtr& received_redirect_head() const {
+    return received_redirect_head_;
+  }
+
  private:
   mojo::Receiver<network::mojom::URLLoaderClient> receiver_{this};
   std::optional<net::RedirectInfo> received_redirect_info_;
+  network::mojom::URLResponseHeadPtr received_response_head_;
+  network::mojom::URLResponseHeadPtr received_redirect_head_;
+  base::OnceClosure on_receive_response_callback_;
   base::OnceClosure on_receive_redirect_callback_;
 };
 
@@ -281,6 +314,10 @@
     return client_.get();
   }
 
+  mojo::ScopedDataPipeConsumerHandle ReleaseConsumerHandle() {
+    return std::move(consumer_);
+  }
+
  protected:
   void SetUp() override {
     ASSERT_EQ(CreateDataPipe(producer_, consumer_), MOJO_RESULT_OK);
@@ -661,4 +698,65 @@
   EXPECT_EQ(owner()->received_redirect_info()->new_url,
             GURL("filesystem:http://example.com/temporary/test"));
 }
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       ForwardedResponseHeadOmitsNavigationOnlyFields) {
+  SetUpURLLoaderClient(network::GetDataPipeDefaultAllocationSize());
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_OK));
+  head->load_timing_internal_info.emplace();
+  head->ssl_info.emplace();
+
+  base::RunLoop run_loop;
+  client_for_fetch_handler()->SetOnReceiveResponseCallback(
+      run_loop.QuitClosure());
+
+  client()->OnReceiveResponse(std::move(head), ReleaseConsumerHandle(),
+                              std::nullopt);
+  CloseConnection();
+  run_loop.Run();
+
+  ASSERT_TRUE(owner()->committed_response_head());
+  EXPECT_TRUE(owner()
+                  ->committed_response_head()
+                  ->load_timing_internal_info.has_value());
+  EXPECT_TRUE(owner()->committed_response_head()->ssl_info.has_value());
+
+  ASSERT_TRUE(client_for_fetch_handler()->received_response_head());
+  EXPECT_FALSE(client_for_fetch_handler()
+                   ->received_response_head()
+                   ->load_timing_internal_info.has_value());
+  EXPECT_FALSE(client_for_fetch_handler()
+                   ->received_response_head()
+                   ->ssl_info.has_value());
+}
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       ForwardedRedirectHeadOmitsNavigationOnlyFields) {
+  SetUpURLLoaderClient(network::GetDataPipeDefaultAllocationSize());
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com/redirected");
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_MOVED_PERMANENTLY));
+  head->load_timing_internal_info.emplace();
+  head->ssl_info.emplace();
+
+  base::RunLoop run_loop;
+  client_for_fetch_handler()->SetOnReceiveRedirectCallback(
+      run_loop.QuitClosure());
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+  run_loop.Run();
+
+  ASSERT_TRUE(client_for_fetch_handler()->received_redirect_head());
+  EXPECT_FALSE(client_for_fetch_handler()
+                   ->received_redirect_head()
+                   ->load_timing_internal_info.has_value());
+  EXPECT_FALSE(client_for_fetch_handler()
+                   ->received_redirect_head()
+                   ->ssl_info.has_value());
+}
 }  // namespace content
Loading diff…

Original Bug Report

reported by vm...@google.com

Information disclosure of LoadTimingInternalInfo and SSLInfo via RaceNetworkRequest

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 potential information disclosure vulnerability exists in the Service Worker RaceNetworkRequest implementation. When handling main-resource navigations, the browser process forwards an unfiltered URLResponseHead containing privileged telemetry and TLS metadata directly to the untrusted renderer process. This allows a compromised Service Worker renderer to extract sensitive connection metrics and certificate verification data.

Affected files:

  • content/common/service_worker/race_network_request_url_loader_client.cc
  • content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
  • content/browser/service_worker/service_worker_main_resource_loader.cc

Estimated timestamp from git blame: 2023-06-16

Summary

A potential information disclosure vulnerability exists in the Service Worker RaceNetworkRequest implementation in Chromium. During a browser-initiated main-frame navigation request, a compromised Service Worker renderer process can bypass the renderer-browser security boundary to obtain browser-privileged metadata. Specifically, it can retrieve LoadTimingInternalInfo (containing internal network configurations, including DNS/DoH configurations) and net::SSLInfo (containing evaluated TLS certificate chains).

While the control-channel requests (e.g., FollowRedirect) are proxy-protected by a URLLoaderProxy to prevent renderer-driven navigation manipulation, the URLLoaderClient data-channel is directly joined to the renderer via Mojo pipe fusion (mojo::FusePipes). The browser process fails to sanitize or strip restricted fields from the URLResponseHead metadata before forwarding it across this channel.

Root Cause Analysis

  1. High-Privilege Response Head Generation In content/browser/service_worker/service_worker_main_resource_loader.cc, the browser process initiates a parallel race network request using options from NavigationURLLoader::GetURLLoaderOptions(). For outermost main frames, this specifies kURLLoadOptionSendSSLInfoWithResponse and attaches trusted_params. In the network service, this triggers the inclusion of LoadTimingInternalInfo and ssl_info in the response head:

    • Source: services/network/url_loader_util.cc (BuildResponseHead lines 711-724)
  2. Direct Pipe Fusion to Renderer In content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc, the URLLoaderClient pipes are directly fused via mojo::FusePipes under the assumption that the data delivery channel only carries benign data:

    bool result = mojo::FusePipes(std::move(client_receiver_), std::move(client));
    

    Here, client_receiver_ is the receiver end of the forwarding remote held by the browser-side client, and client is the client remote supplied by the untrusted renderer process.

  3. Unsanitized Forwarding When the browser process’s ServiceWorkerRaceNetworkRequestURLLoaderClient receives the response, it clones and forwards the URLResponseHead verbatim to the forwarding_client_ remote without stripping the sensitive fields:

    • Source: content/common/service_worker/race_network_request_url_loader_client.cc (ForwardResponseToClient lines 725-726)

Potential Steps to Trigger the Issue

(Note: These are potential steps; our tooling does not currently have the capability to run code to verify with a live exploit or Proof of Concept)

  1. An attacker registers or compromises a Service Worker on a target scope (e.g., evil.example) configured to use RaceNetworkRequest (e.g., via the Static Router API).
  2. A victim navigates to https://evil.example/index.html (outermost main-frame navigation).
  3. The browser triggers the parallel RaceNetworkRequest with browser-level privileges and options, causing the network process to include load_timing_internal_info and ssl_info in the URLResponseHead returned.
  4. The compromised Service Worker renderer calls CreateLoaderAndStart on the forwarded factory to trigger Mojo pipe fusion.
  5. The browser-side client forwards the unsanitized URLResponseHead containing load_timing_internal_info and ssl_info to the renderer.
  6. The compromised renderer reads the deserialized response head to exfiltrate DNS/DoH configurations and internal TLS certificate chains.

Suggested Remediation

To enforce the browser-renderer boundary, the browser process must sanitize/strip sensitive fields from the URLResponseHead before forwarding it to the untrusted renderer in ServiceWorkerRaceNetworkRequestURLLoaderClient::ForwardResponseToClient:

void ServiceWorkerRaceNetworkRequestURLLoaderClient::ForwardResponseToClient(
    network::mojom::URLResponseHeadPtr head,
    mojo::ScopedDataPipeConsumerHandle body,
    std::optional<mojo_base::BigBuffer> cached_metadata) {
  CHECK(!has_forwarded_response_);
  has_forwarded_response_ = true;

  // Strip browser-privileged information before crossing the trust boundary
  if (head) {
    head->load_timing_internal_info.reset();
    head->ssl_info.reset();
  }

  forwarding_client_->OnReceiveResponse(std::move(head), std::move(body),
                                        std::move(cached_metadata));
}

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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