Medium watch CVSS 6.5 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in ServiceWorker
DescriptionInformation leak in ServiceWorker
ComponentServiceWorker
Sub-componentServiceWorker
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517634590
Fix commit6784eed9c650 (chromium/src) +117/-4
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

Background

RaceNetworkRequest
A service-worker optimization where the browser races a direct network request against the fetch handler; ServiceWorkerRaceNetworkRequestURLLoaderClient drives the network side and can forward events to the fetch handler in the renderer.
`OnReceiveRedirect` / `redirect_info.new_url`
The URLLoaderClient callback delivering a redirect, whose new_url is the redirect target and may point at a non-HTTP(S) scheme such as filesystem:.
`forwarding_client_`
The client connected to the fetch handler in the renderer process, to which redirect events are relayed.
`CorsURLLoader` censoring
For renderer-destined manual redirects, CorsURLLoader normally replaces non-HTTP(S) redirect targets with data:, so the renderer never sees the raw URL; browser navigations skip this censoring.

Root Cause Analysis

ServiceWorkerRaceNetworkRequestURLLoaderClient::OnReceiveRedirect forwarded the network-provided redirect_info verbatim to forwarding_client_->OnReceiveRedirect, which is connected to the fetch handler in the renderer. For main-resource race requests the upstream loader runs as a browser navigation, so CorsURLLoader did not censor non-HTTP(S) redirect targets, and the raw redirect_info.new_url (e.g. a filesystem: URL) reached the renderer. This violated the invariant that a renderer-facing fetch handler must not observe non-HTTP(S) redirect targets it could not otherwise see.

The fix copies redirect_info into forwarding_redirect_info and, when !redirect_info.new_url.SchemeIsHTTPOrHTTPS(), overwrites new_url with GURL("data:,") before forwarding in both the kNoResponseYet/kSubresourceLoaderIsHandlingRedirect and kServiceWorker cases. The original, uncensored redirect_info is still passed to owner_ in the kWithoutServiceWorker case, which runs in the browser process and legitimately needs the real target; the regression tests confirm the fetch handler receives data:, for a filesystem: target but the real URL for an https: target, and that the browser-handled path still gets the real URL.

Key insight
The core mistake was forwarding a browser-navigation redirect’s raw non-HTTP(S) new_url to the renderer fetch handler without the CorsURLLoader censoring that renderer-destined redirects normally receive; the fix replaces non-HTTP(S) targets with data:, on the renderer-forwarding path only.

Attack Path

  1. Register a service worker with a fetch handler An attacker controls a page with a service worker whose fetch handler observes redirect events for a raced main-resource request.
  2. Trigger a non-HTTP(S) redirect A raced request resolves to a redirect whose target is a non-HTTP(S) scheme such as filesystem:http://example.com/temporary/test.
  3. Skip CORS censoring Because the main-resource race runs as a browser navigation, CorsURLLoader does not sanitize the target, and OnReceiveRedirect forwarded the raw redirect_info to the fetch handler.
  4. Read the leaked URL The fetch handler in the renderer observes the full non-HTTP(S) redirect target it should not have access to, disclosing information about the redirect destination.

Impact Assessment

An attacker-controlled service-worker fetch handler running in the renderer could learn the raw non-HTTP(S) target of a redirect during a raced main-resource request, an information leak of a URL the renderer would normally not be permitted to see. The precondition is a registered service worker with a fetch handler and a raced request that redirects to a non-HTTP(S) scheme. The advisory classifies this as an information leak in ServiceWorker with no exploited-in-the-wild status stated; no memory-safety or escalation impact is indicated by the patch.

Changed Functions

FunctionChangeNotes
if
content/common/service_worker/race_network_request_url_loader_client_unittest.cc
modified
ServiceWorkerRaceNetworkRequestURLLoaderClientTest
content/common/service_worker/race_network_request_url_loader_client_unittest.cc
modified

Files Changed

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

Audit Directions

  • Browser-navigation loaders bypassing CORS censoring
    Look for code paths that forward redirect_info or response metadata to a renderer without going through CorsURLLoader, since navigation-mode loaders skip the usual non-HTTP(S) sanitization.
  • Scheme checks on forwarded URLs
    Audit other forwarding_client_/renderer-destined URLLoaderClient relays for missing SchemeIsHTTPOrHTTPS() gating on redirect or response URLs.
  • Divergent browser vs. renderer data paths
    Review handlers that reuse one redirect_info/response object for both a browser-side consumer and a renderer-side consumer, ensuring the renderer copy is censored while the browser copy stays intact.
From 6784eed9c650a156e305f2b7028de08afccbcc72 Mon Sep 17 00:00:00 2001
From: Shunya Shishido <sisidovski@chromium.org>
Date: Wed, 22 Jul 2026 23:47:38 -0700
Subject: [PATCH] ServiceWorker: Censor non-HTTP(S) redirect URL in race forward

ServiceWorkerRaceNetworkRequestURLLoaderClient forwards the network
redirect_info to forwarding_client_, which is connected to the fetch
handler in the renderer. For main resource race requests the upstream
loader runs as a browser navigation, so CorsURLLoader does not censor
non-HTTP(S) redirect targets and the raw URL was forwarded.

Replace redirect_info.new_url with "data:," for non-HTTP(S) schemes
before forwarding to the fetch handler, matching the behaviour
CorsURLLoader applies for renderer-destined manual redirects. The
original redirect_info is still passed to owner_->HandleRedirect() in
the kWithoutServiceWorker case, which runs in the browser and needs the
real target.

TAG=agy
CONV=569bdb49-a155-4b4d-9e86-7de205c02f43

Bug: 517634590
Change-Id: I6bd1a238ab6e6c06e7c9c81ca897cce58e2f8124
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8130660
Commit-Queue: Shunya Shishido <sisidovski@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1666861}
---
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 0f79c1dc..ecd9965 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
@@ -22,6 +22,7 @@
 #include "mojo/public/cpp/system/handle_signals_state.h"
 #include "net/http/http_response_headers.h"
 #include "net/http/http_status_code.h"
+#include "net/url_request/redirect_info.h"
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/header_util.h"
 #include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
@@ -233,12 +234,23 @@
   // |owner| as a RaceNetworkResponse's response, we stop the race and back the
   // response to the fetch handler only instead, so that we guarantee the fetch
   // handler completion.
+  // For robustness, ensure non-HTTP(S) redirect URLs are replaced with "data:,"
+  // before forwarding to the client in the renderer process, matching the
+  // behavior of manual redirects handled by CorsURLLoader. The original
+  // redirect info is still passed to `owner_` in the kWithoutServiceWorker
+  // case where the redirect is handled directly within the browser process.
+  net::RedirectInfo forwarding_redirect_info = redirect_info;
+  if (!redirect_info.new_url.SchemeIsHTTPOrHTTPS()) {
+    forwarding_redirect_info.new_url = GURL("data:,");
+  }
+
   switch (owner_->commit_responsibility()) {
     case FetchResponseFrom::kNoResponseYet:
     case FetchResponseFrom::kSubresourceLoaderIsHandlingRedirect:
       // This happens when the response is faster than the fetch handler.
       owner_->SetCommitResponsibility(FetchResponseFrom::kServiceWorker);
-      forwarding_client_->OnReceiveRedirect(redirect_info, std::move(head));
+      forwarding_client_->OnReceiveRedirect(forwarding_redirect_info,
+                                            std::move(head));
       MaybeCompleteRedirectResponse(/*run_completion_callback=*/false);
       return;
     case FetchResponseFrom::kServiceWorker:
@@ -247,7 +259,8 @@
       // 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.
-      forwarding_client_->OnReceiveRedirect(redirect_info, std::move(head));
+      forwarding_client_->OnReceiveRedirect(forwarding_redirect_info,
+                                            std::move(head));
       MaybeCompleteRedirectResponse(/*run_completion_callback=*/true);
       return;
     case FetchResponseFrom::kWithoutServiceWorker:
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 6ea37d5..b93b89f 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
@@ -16,6 +16,7 @@
 #include "mojo/public/cpp/system/data_pipe.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
 #include "net/base/net_errors.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/url_loader.mojom.h"
@@ -77,7 +78,13 @@
   }
   void HandleRedirect(
       const net::RedirectInfo& redirect_info,
-      const network::mojom::URLResponseHeadPtr& response_head) override {}
+      const network::mojom::URLResponseHeadPtr& response_head) override {
+    received_redirect_info_ = redirect_info;
+  }
+
+  const std::optional<net::RedirectInfo>& received_redirect_info() const {
+    return received_redirect_info_;
+  }
 
   base::WeakPtr<MockServiceWorkerResourceLoader> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
@@ -94,6 +101,7 @@
  private:
   OnCommitResponseCallback on_commit_response_;
   OnCompletedCallback on_commit_completed_;
+  std::optional<net::RedirectInfo> received_redirect_info_;
   base::WeakPtrFactory<MockServiceWorkerResourceLoader> weak_factory_{this};
 };
 
@@ -204,15 +212,30 @@
     WatchResponseBody(head, std::move(body));
   }
   void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
-                         network::mojom::URLResponseHeadPtr head) override {}
+                         network::mojom::URLResponseHeadPtr head) override {
+    received_redirect_info_ = redirect_info;
+    if (on_receive_redirect_callback_) {
+      std::move(on_receive_redirect_callback_).Run();
+    }
+  }
   void OnUploadProgress(int64_t current_position,
                         int64_t total_size,
                         base::OnceCallback<void()> callback) override {}
   void OnTransferSizeUpdated(int32_t transfer_size_diff) override {}
   void OnComplete(const network::URLLoaderCompletionStatus& status) override {}
 
+  void SetOnReceiveRedirectCallback(base::OnceClosure callback) {
+    on_receive_redirect_callback_ = std::move(callback);
+  }
+
+  const std::optional<net::RedirectInfo>& received_redirect_info() const {
+    return received_redirect_info_;
+  }
+
  private:
   mojo::Receiver<network::mojom::URLLoaderClient> receiver_{this};
+  std::optional<net::RedirectInfo> received_redirect_info_;
+  base::OnceClosure on_receive_redirect_callback_;
 };
 
 class ServiceWorkerRaceNetworkRequestURLLoaderClientTest
@@ -254,6 +277,10 @@
     return client_for_fetch_handler_.get();
   }
 
+  ServiceWorkerRaceNetworkRequestURLLoaderClient* client() {
+    return client_.get();
+  }
+
  protected:
   void SetUp() override {
     ASSERT_EQ(CreateDataPipe(producer_, consumer_), MOJO_RESULT_OK);
@@ -561,4 +588,77 @@
   EXPECT_EQ(client_state(),
             ServiceWorkerRaceNetworkRequestURLLoaderClient::State::kCompleted);
 }
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       RedirectForwardedToFetchHandlerForNonHttpScheme) {
+  const uint32_t data_pipe_capacity_num_bytes = 8;
+  SetUpURLLoaderClient(data_pipe_capacity_num_bytes);
+
+  base::RunLoop run_loop;
+  client_for_fetch_handler()->SetOnReceiveRedirectCallback(
+      run_loop.QuitClosure());
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("filesystem:http://example.com/temporary/test");
+  redirect_info.status_code = 302;
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_FOUND));
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+  run_loop.Run();
+
+  ASSERT_TRUE(client_for_fetch_handler()->received_redirect_info().has_value());
+  EXPECT_EQ(client_for_fetch_handler()->received_redirect_info()->new_url,
+            GURL("data:,"));
+}
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       RedirectForwardedToFetchHandlerForHttpScheme) {
+  const uint32_t data_pipe_capacity_num_bytes = 8;
+  SetUpURLLoaderClient(data_pipe_capacity_num_bytes);
+
+  base::RunLoop run_loop;
+  client_for_fetch_handler()->SetOnReceiveRedirectCallback(
+      run_loop.QuitClosure());
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com/redirected");
+  redirect_info.status_code = 302;
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_FOUND));
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+  run_loop.Run();
+
+  ASSERT_TRUE(client_for_fetch_handler()->received_redirect_info().has_value());
+  EXPECT_EQ(client_for_fetch_handler()->received_redirect_info()->new_url,
+            GURL("https://example.com/redirected"));
+}
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       RedirectHandledByOwnerForNonHttpScheme) {
+  const uint32_t data_pipe_capacity_num_bytes = 8;
+  SetUpURLLoaderClient(data_pipe_capacity_num_bytes);
+
+  owner()->SetCommitResponsibility(
+      ServiceWorkerResourceLoader::FetchResponseFrom::kWithoutServiceWorker);
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("filesystem:http://example.com/temporary/test");
+  redirect_info.status_code = 302;
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_FOUND));
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+
+  ASSERT_TRUE(owner()->received_redirect_info().has_value());
+  EXPECT_EQ(owner()->received_redirect_info()->new_url,
+            GURL("filesystem:http://example.com/temporary/test"));
+}
 }  // namespace content
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 6ea37d5..b93b89f 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
@@ -16,6 +16,7 @@
 #include "mojo/public/cpp/system/data_pipe.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
 #include "net/base/net_errors.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/url_loader.mojom.h"
@@ -77,7 +78,13 @@
   }
   void HandleRedirect(
       const net::RedirectInfo& redirect_info,
-      const network::mojom::URLResponseHeadPtr& response_head) override {}
+      const network::mojom::URLResponseHeadPtr& response_head) override {
+    received_redirect_info_ = redirect_info;
+  }
+
+  const std::optional<net::RedirectInfo>& received_redirect_info() const {
+    return received_redirect_info_;
+  }
 
   base::WeakPtr<MockServiceWorkerResourceLoader> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
@@ -94,6 +101,7 @@
  private:
   OnCommitResponseCallback on_commit_response_;
   OnCompletedCallback on_commit_completed_;
+  std::optional<net::RedirectInfo> received_redirect_info_;
   base::WeakPtrFactory<MockServiceWorkerResourceLoader> weak_factory_{this};
 };
 
@@ -204,15 +212,30 @@
     WatchResponseBody(head, std::move(body));
   }
   void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
-                         network::mojom::URLResponseHeadPtr head) override {}
+                         network::mojom::URLResponseHeadPtr head) override {
+    received_redirect_info_ = redirect_info;
+    if (on_receive_redirect_callback_) {
+      std::move(on_receive_redirect_callback_).Run();
+    }
+  }
   void OnUploadProgress(int64_t current_position,
                         int64_t total_size,
                         base::OnceCallback<void()> callback) override {}
   void OnTransferSizeUpdated(int32_t transfer_size_diff) override {}
   void OnComplete(const network::URLLoaderCompletionStatus& status) override {}
 
+  void SetOnReceiveRedirectCallback(base::OnceClosure callback) {
+    on_receive_redirect_callback_ = std::move(callback);
+  }
+
+  const std::optional<net::RedirectInfo>& received_redirect_info() const {
+    return received_redirect_info_;
+  }
+
  private:
   mojo::Receiver<network::mojom::URLLoaderClient> receiver_{this};
+  std::optional<net::RedirectInfo> received_redirect_info_;
+  base::OnceClosure on_receive_redirect_callback_;
 };
 
 class ServiceWorkerRaceNetworkRequestURLLoaderClientTest
@@ -254,6 +277,10 @@
     return client_for_fetch_handler_.get();
   }
 
+  ServiceWorkerRaceNetworkRequestURLLoaderClient* client() {
+    return client_.get();
+  }
+
  protected:
   void SetUp() override {
     ASSERT_EQ(CreateDataPipe(producer_, consumer_), MOJO_RESULT_OK);
@@ -561,4 +588,77 @@
   EXPECT_EQ(client_state(),
             ServiceWorkerRaceNetworkRequestURLLoaderClient::State::kCompleted);
 }
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       RedirectForwardedToFetchHandlerForNonHttpScheme) {
+  const uint32_t data_pipe_capacity_num_bytes = 8;
+  SetUpURLLoaderClient(data_pipe_capacity_num_bytes);
+
+  base::RunLoop run_loop;
+  client_for_fetch_handler()->SetOnReceiveRedirectCallback(
+      run_loop.QuitClosure());
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("filesystem:http://example.com/temporary/test");
+  redirect_info.status_code = 302;
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_FOUND));
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+  run_loop.Run();
+
+  ASSERT_TRUE(client_for_fetch_handler()->received_redirect_info().has_value());
+  EXPECT_EQ(client_for_fetch_handler()->received_redirect_info()->new_url,
+            GURL("data:,"));
+}
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       RedirectForwardedToFetchHandlerForHttpScheme) {
+  const uint32_t data_pipe_capacity_num_bytes = 8;
+  SetUpURLLoaderClient(data_pipe_capacity_num_bytes);
+
+  base::RunLoop run_loop;
+  client_for_fetch_handler()->SetOnReceiveRedirectCallback(
+      run_loop.QuitClosure());
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com/redirected");
+  redirect_info.status_code = 302;
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_FOUND));
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+  run_loop.Run();
+
+  ASSERT_TRUE(client_for_fetch_handler()->received_redirect_info().has_value());
+  EXPECT_EQ(client_for_fetch_handler()->received_redirect_info()->new_url,
+            GURL("https://example.com/redirected"));
+}
+
+TEST_F(ServiceWorkerRaceNetworkRequestURLLoaderClientTest,
+       RedirectHandledByOwnerForNonHttpScheme) {
+  const uint32_t data_pipe_capacity_num_bytes = 8;
+  SetUpURLLoaderClient(data_pipe_capacity_num_bytes);
+
+  owner()->SetCommitResponsibility(
+      ServiceWorkerResourceLoader::FetchResponseFrom::kWithoutServiceWorker);
+
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("filesystem:http://example.com/temporary/test");
+  redirect_info.status_code = 302;
+  redirect_info.new_method = "GET";
+
+  network::mojom::URLResponseHeadPtr head(
+      network::CreateURLResponseHead(net::HTTP_FOUND));
+
+  client()->OnReceiveRedirect(redirect_info, std::move(head));
+
+  ASSERT_TRUE(owner()->received_redirect_info().has_value());
+  EXPECT_EQ(owner()->received_redirect_info()->new_url,
+            GURL("filesystem:http://example.com/temporary/test"));
+}
 }  // namespace content
Loading diff…

Original Bug Report

reported by vm...@google.com

Bypass of manual redirect URL censoring in Service Worker race-network-request

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 security boundary violation in the Service Worker race-network-request path potentially allows a compromised renderer to observe uncensored non-HTTP(S) redirect targets in manual redirect mode. This bypass occurs because the underlying loader factory is created with browser-process privileges, whereas the client data pipe is fused directly to the renderer. Consequently, manual redirect censoring in the network process is skipped, potentially leaking internal redirect targets.

Affected files:

  • services/network/cors/cors_url_loader.cc
  • content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
  • content/browser/loader/navigation_url_loader_impl.cc
  • content/common/service_worker/race_network_request_url_loader_client.cc
  • content/browser/service_worker/service_worker_client.cc

Estimated timestamp from git blame: 2026-02-17

Root Cause Analysis

In CorsURLLoader::OnReceiveRedirect, when redirect_mode == kManual, any non-HTTP(S) redirect_info.new_url is designed to be censored to "data:," before being forwarded to the client, unless the loader believes the navigation is browser-initiated:

// services/network/cors/cors_url_loader.cc
if (request_.redirect_mode == mojom::RedirectMode::kManual) {
  ...
  net::RedirectInfo censored_redirect_info = redirect_info;
  const bool is_browser_navigation =
      request_.mode == mojom::RequestMode::kNavigate &&
      process_id_ == OriginatingProcessId::browser();
  if (!is_browser_navigation &&
      !redirect_info.new_url.SchemeIsHTTPOrHTTPS()) {
    censored_redirect_info.new_url = GURL("data:,");
  }
  ...
  forwarding_client_->OnReceiveRedirect(censored_redirect_info,
                                        std::move(response_head));

This censoring acts as a defense-in-depth mechanism to protect the renderer from observing sensitive non-HTTP(S) redirect targets (such as internal schemes, custom protocols, or extension URLs).

However, for a Service Worker race-network-request main-resource navigation, this censoring check is bypassed. The loader factory for the race request is created via ServiceWorkerClient::CreateNetworkURLLoaderFactory and NavigationURLLoaderImpl::CreateURLLoaderFactoryWithHeaderClient, which configures the factory with browser process credentials:

// content/browser/loader/navigation_url_loader_impl.cc
network::mojom::URLLoaderFactoryParamsPtr params =
    network::mojom::URLLoaderFactoryParams::New();
params->header_client = std::move(header_client);
params->process_id = network::OriginatingProcessId::browser();
params->is_trusted = true;

Because process_id is set to OriginatingProcessId::browser(), the CorsURLLoader evaluates is_browser_navigation to true and skips the redirect URL censoring, assuming the redirect will be handled safely within the browser process. In reality, the client data-pipe of the race-network-request is eventually fused directly to the renderer’s URLLoaderClient in ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory::CreateLoaderAndStart using mojo::FusePipes.

Furthermore, when the manual redirect is delivered to the renderer in manual redirect mode, the response’s type is FetchResponseType::kOpaqueRedirect. In MojoURLLoaderClient::OnReceiveRedirect, the renderer explicitly skips calling Platform::Current()->IsRedirectSafe for manual/opaque redirects, meaning the renderer-side scheme safety checks are completely bypassed.

Potential Steps to Trigger / Exploit

Note: Our tooling does not currently have the capability to execute code, so these are potential steps based on code analysis.

  1. A compromised renderer registers a Service Worker scope that includes a static routing rule configured with a router source of ServiceWorkerRouterSourceType::kRaceNetworkAndFetchEvent (the race-network-and-fetch-handler source).
  2. The renderer triggers a main-resource navigation to a URL within the Service Worker’s scope.
  3. The browser intercepts the navigation, matches the rule, and starts the race-network-request concurrently by allocating a forwarding pipe pair.
  4. The browser passes the forwarded factory remote to the Service Worker renderer. The Service Worker calls fetch() on the race request token, which triggers mojo::FusePipes on the client pipe, linking the browser’s forwarding client directly to the renderer’s URLLoaderClient.
  5. During the network load of the race request, a manual redirect to a sensitive non-HTTP(S) target URL (such as an internal or extension URL rewritten by browser-side extension APIs) occurs.
  6. Because the network factory was created with OriginatingProcessId::browser(), CorsURLLoader::OnReceiveRedirect skips manual redirect censoring.
  7. The uncensored redirect_info is dispatched via forwarding_client_->OnReceiveRedirect and sent directly through the fused client pipe to the renderer.
  8. The renderer process reads the raw, uncensored non-HTTP(S) redirect target URL, bypassing the manual redirect URL censoring defense-in-depth protection.

Proposed Fix

To remediate this issue, ensure that Service Worker race-network-requests (which are intended to be exposed to the renderer) do not inherit browser-process privileges in a way that bypasses manual redirect censoring.

One potential solution is to explicitly identify whether a Navigation loader factory is being generated for a Service Worker race-network-request and, if so, omit setting params->process_id to OriginatingProcessId::browser(), or explicitly apply manual redirect URL censoring within ServiceWorkerRaceNetworkRequestURLLoaderClient::OnReceiveRedirect before forwarding the redirect info to the forwarding_client_ interface.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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