Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in ServiceWorker
DescriptionInsufficient policy enforcement in ServiceWorker
ComponentServiceWorker
Bug ClassLogic Error
Tracker497436273
Fix commite2b91876eb01 (chromium/src) +497/-108
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-19

Changed Functions

FunctionChangeNotes
if
content/browser/service_worker/service_worker_container_host.cc
modified
CreateDocumentIsolationPolicyInfo
content/browser/service_worker/service_worker_container_host.cc
modified
cross_origin_embedder_policy_reporter
content/browser/service_worker/service_worker_container_host.h
modified
document_isolation_policy_reporter
content/browser/service_worker/service_worker_container_host.h
modified

Files Changed

  • content/browser/service_worker/service_worker_container_host.cc
  • content/browser/service_worker/service_worker_container_host.h
  • content/browser/service_worker/service_worker_main_resource_loader.cc
From e2b91876eb013ceb7bc7ee5cab2fa44c4f45df16 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
Date: Sun, 12 Apr 2026 19:16:13 -0700
Subject: [PATCH] Enforce CORP for Static Router Cache Source

This CL ensures that Cross-Origin Resource Policy (CORP) is correctly
enforced and reported when using the Service Worker Static Routing API's
cache source, gated by a feature flag.

Key changes:
1. Feature Flag: Introduced kServiceWorkerStaticRouterCORPCheck
   to gate the new blocking behavior in content/common/features.h.
2. UMA: Added ServiceWorker.StaticRouter.{Subresource|MainResource}.CORPCheckResult
   to monitor policy violations in both enforcement and shadow modes.
3. Reporting: Pass the client's COEP/DIP Reporters from the browser to
   the renderer via ControllerServiceWorkerInfo, ensuring security
   violations are properly reported.
4. Unit Tests: Added comprehensive unit tests in
   ServiceWorkerResourceLoaderTest to verify CORP blocking logic and
   UMA recording across different scenarios.
5. Testing Config: Added fieldtrial_testing_config.json entry to enable
   the feature for automated testing.
6. Refactoring: Encapsulated policy and reporter information into Mojo
   structs (CrossOriginEmbedderPolicyInfo and
   DocumentIsolationPolicyInfo) to ensure type safety and consistent
   handling. Replaced acronyms with full names (e.g., coep ->
   cross_origin_embedder_policy) for better readability.

DanglingUntriaged-notes: In WebEmbeddedWorkerImplTest, the mock client
holds a pointer to the proxy which can be destroyed first during
termination tests. This is a known lifecycle pattern in this specific
unit test environment.

Bug: 497436273
Change-Id: Iaf3b66daa02573637a196e26a54e0816cf7b87f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7725522
Reviewed-by: Rakina Zata Amni <[email protected]>
Reviewed-by: Shunya Shishido <[email protected]>
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
Reviewed-by: Hidehiko Abe <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1613472}
---

diff --git a/content/browser/service_worker/service_worker_container_host.cc b/content/browser/service_worker/service_worker_container_host.cc
index defb9876..72bbd04 100644
--- a/content/browser/service_worker/service_worker_container_host.cc
+++ b/content/browser/service_worker/service_worker_container_host.cc
@@ -86,17 +86,19 @@
     blink::mojom::ServiceWorkerContainerInfoForClientPtr& container_info,
     const PolicyContainerPolicies& policy_container_policies,
     mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
-        coep_reporter,
+        cross_origin_embedder_policy_reporter,
     mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
-        dip_reporter,
+        document_isolation_policy_reporter,
     ukm::SourceId ukm_source_id)
     : service_worker_client_(std::move(service_worker_client)),
       container_(
           container_info->client_receiver.InitWithNewEndpointAndPassRemote()),
       ukm_source_id_(std::move(ukm_source_id)),
       policy_container_policies_(policy_container_policies.Clone()),
-      coep_reporter_(std::move(coep_reporter)),
-      dip_reporter_(std::move(dip_reporter)) {
+      cross_origin_embedder_policy_reporter_(
+          std::move(cross_origin_embedder_policy_reporter)),
+      document_isolation_policy_reporter_(
+          std::move(document_isolation_policy_reporter)) {
   CHECK(container_.is_bound());
   CHECK(service_worker_client_);
   CHECK(!service_worker_client_->is_response_committed());
@@ -479,6 +481,11 @@
       controller_info->router_data->initial_running_status =
           controller()->running_status();
     }
+
+    controller_info->cross_origin_embedder_policy =
+        CreateCrossOriginEmbedderPolicyInfo();
+    controller_info->document_isolation_policy =
+        CreateDocumentIsolationPolicyInfo();
   }
 
   // Note that |controller_info->remote_controller| is null if the controller
@@ -705,26 +712,9 @@
 
 void ServiceWorkerContainerHostForClient::CloneControllerServiceWorker(
     mojo::PendingReceiver<blink::mojom::ControllerServiceWorker> receiver) {
-  mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
-      coep_reporter_to_be_passed;
-  mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
-      dip_reporter_to_be_passed;
-  if (coep_reporter_) {
-    coep_reporter_->Clone(
-        coep_reporter_to_be_passed.InitWithNewPipeAndPassReceiver());
-  }
-
-  if (dip_reporter_) {
-    dip_reporter_->Clone(
-        dip_reporter_to_be_passed.InitWithNewPipeAndPassReceiver());
-  }
-
-  controller()->controller()->Clone(
-      std::move(receiver),
-      policy_container_policies_.cross_origin_embedder_policy,
-      std::move(coep_reporter_to_be_passed),
-      policy_container_policies_.document_isolation_policy,
-      std::move(dip_reporter_to_be_passed));
+  controller()->controller()->Clone(std::move(receiver),
+                                    CreateCrossOriginEmbedderPolicyInfo(),
+                                    CreateDocumentIsolationPolicyInfo());
 }
 
 bool ServiceWorkerContainerHostForClient::AllowServiceWorker(
@@ -1391,4 +1381,28 @@
   return service_worker_client().controller();
 }
 
+blink::mojom::CrossOriginEmbedderPolicyInfoPtr
+ServiceWorkerContainerHostForClient::CreateCrossOriginEmbedderPolicyInfo()
+    const {
+  auto info = blink::mojom::CrossOriginEmbedderPolicyInfo::New(
+      policy_container_policies_.cross_origin_embedder_policy,
+      mojo::NullRemote());
+  if (cross_origin_embedder_policy_reporter_) {
+    cross_origin_embedder_policy_reporter_->Clone(
+        info->reporter.InitWithNewPipeAndPassReceiver());
+  }
+  return info;
+}
+
+blink::mojom::DocumentIsolationPolicyInfoPtr
+ServiceWorkerContainerHostForClient::CreateDocumentIsolationPolicyInfo() const {
+  auto info = blink::mojom::DocumentIsolationPolicyInfo::New(
+      policy_container_policies_.document_isolation_policy, mojo::NullRemote());
+  if (document_isolation_policy_reporter_) {
+    document_isolation_policy_reporter_->Clone(
+        info->reporter.InitWithNewPipeAndPassReceiver());
+  }
+  return info;
+}
+
 }  // namespace content
diff --git a/content/browser/service_worker/service_worker_container_host.h b/content/browser/service_worker/service_worker_container_host.h
index 7f857d8..1b39ef85 100644
--- a/content/browser/service_worker/service_worker_container_host.h
+++ b/content/browser/service_worker/service_worker_container_host.h
@@ -328,7 +328,25 @@
   ukm::SourceId ukm_source_id() const { return ukm_source_id_; }
   ServiceWorkerVersion* controller() const;
 
+  const mojo::Remote<network::mojom::CrossOriginEmbedderPolicyReporter>&
+  cross_origin_embedder_policy_reporter() const {
+    return cross_origin_embedder_policy_reporter_;
+  }
+  const mojo::Remote<network::mojom::DocumentIsolationPolicyReporter>&
+  document_isolation_policy_reporter() const {
+    return document_isolation_policy_reporter_;
+  }
+
+  const PolicyContainerPolicies& policy_container_policies() const {
+    return policy_container_policies_;
+  }
+
  private:
+  blink::mojom::CrossOriginEmbedderPolicyInfoPtr
+  CreateCrossOriginEmbedderPolicyInfo() const;
+  blink::mojom::DocumentIsolationPolicyInfoPtr
+  CreateDocumentIsolationPolicyInfo() const;
+
   // Callback for ServiceWorkerContextCore::RegisterServiceWorker().
   void RegistrationComplete(const GURL& script_url,
                             const GURL& scope,
@@ -438,11 +456,12 @@
   // An endpoint connected to the COEP reporter. A clone of this connection is
   // passed to the service worker. Bound on response commit.
   mojo::Remote<network::mojom::CrossOriginEmbedderPolicyReporter>
-      coep_reporter_;
+      cross_origin_embedder_policy_reporter_;
 
   // An endpoint connected to the DocumentIsolationPolicy reporter. A clone of
   // this connection is passed to the service worker. Bound on response commit.
-  mojo::Remote<network::mojom::DocumentIsolationPolicyReporter> dip_reporter_;
+  mojo::Remote<network::mojom::DocumentIsolationPolicyReporter>
+      document_isolation_policy_reporter_;
 
   base::WeakPtrFactory<ServiceWorkerContainerHostForClient> weak_ptr_factory_{
       this};
diff --git a/content/browser/service_worker/service_worker_main_resource_loader.cc b/content/browser/service_worker/service_worker_main_resource_loader.cc
index 25e6685..650a544a 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader.cc
@@ -25,7 +25,9 @@
 #include "base/trace_event/trace_event.h"
 #include "content/browser/loader/navigation_url_loader.h"
 #include "content/browser/loader/response_head_update_params.h"
+#include "content/browser/renderer_host/policy_container_host.h"
 #include "content/browser/service_worker/service_worker_client.h"
+#include "content/browser/service_worker/service_worker_container_host.h"
 #include "content/browser/service_worker/service_worker_context_core.h"
 #include "content/browser/service_worker/service_worker_context_wrapper.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/common/service_worker/service_worker_resource_loader_unittest.cc b/content/common/service_worker/service_worker_resource_loader_unittest.cc
index 1a4fe5c..fd2f132 100644
--- a/content/common/service_worker/service_worker_resource_loader_unittest.cc
+++ b/content/common/service_worker/service_worker_resource_loader_unittest.cc
@@ -4,12 +4,46 @@
 
 #include "content/common/service_worker/service_worker_resource_loader.h"
 
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "content/common/features.h"
+#include "content/public/common/content_features.h"
+#include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/mojom/fetch/fetch_api_response.mojom.h"
 
 namespace content {
 
+namespace {
+
+class TestServiceWorkerResourceLoader : public ServiceWorkerResourceLoader {
+ public:
+  explicit TestServiceWorkerResourceLoader(bool is_main_resource)
+      : is_main_resource_(is_main_resource) {}
+  ~TestServiceWorkerResourceLoader() override = default;
+
+  bool IsMainResourceLoader() override { return is_main_resource_; }
+
+  void CommitResponseBody(
+      const network::mojom::URLResponseHeadPtr& response_head,
+      mojo::ScopedDataPipeConsumerHandle response_body,
+      std::optional<mojo_base::BigBuffer> cached_metadata) override {}
+
+  void CommitEmptyResponseAndComplete() override {}
+
+  void CommitCompleted(int error_code, const char* reason) override {}
+
+  void HandleRedirect(
+      const net::RedirectInfo& redirect_info,
+      const network::mojom::URLResponseHeadPtr& response_head) override {}
+
+ private:
+  bool is_main_resource_;
+};
+
+}  // namespace
+
 TEST(ServiceWorkerResourceLoaderTest, IsValidServiceWorkerResponse) {
   auto request_mode = network::mojom::RequestMode::kSameOrigin;
   auto redirect_mode = network::mojom::RedirectMode::kFollow;
@@ -99,4 +133,80 @@
   }
 }
 
+TEST(ServiceWorkerResourceLoaderTest, IsValidStaticRouterResponse) {
+  TestServiceWorkerResourceLoader loader(/*is_main_resource=*/false);
+  base::HistogramTester histogram_tester;
+
+  network::ResourceRequest request;
+  request.url = GURL("https://b.test/resource");
+  request.request_initiator = url::Origin::Create(GURL("https://a.test/"));
+  request.mode = network::mojom::RequestMode::kNoCors;
+  request.destination = network::mojom::RequestDestination::kImage;
+
+  auto response = blink::mojom::FetchAPIResponse::New();
+  response->response_type = network::mojom::FetchResponseType::kOpaque;
+
+  network::CrossOriginEmbedderPolicy coep;
+  coep.value = network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
+  network::DocumentIsolationPolicy dip;
+
+  // Case 1: Same-origin request should be valid even without CORP.
+  {
+    network::ResourceRequest same_origin_request;
+    same_origin_request.url = GURL("https://a.test/resource");
+    same_origin_request.request_initiator =
+        url::Origin::Create(GURL("https://a.test/"));
+    same_origin_request.mode = network::mojom::RequestMode::kNoCors;
+
+    EXPECT_TRUE(loader.IsValidStaticRouterResponse(
+        same_origin_request, response, coep, nullptr, dip, nullptr));
+    histogram_tester.ExpectBucketCount(
+        "ServiceWorker.StaticRouter.Subresource.CORPCheckResult",
+        ServiceWorkerResourceLoader::CORPCheckResult::kSuccess, 1);
+  }
+
+  // Case 2: Cross-origin request without CORP, flag OFF.
+  {
+    base::test::ScopedFeatureList scoped_feature_list;
+    scoped_feature_list.InitAndDisableFeature(
+        features::kServiceWorkerStaticRouterCORPCheck);
+
+    EXPECT_TRUE(loader.IsValidStaticRouterResponse(request, response, coep,
+                                                   nullptr, dip, nullptr));
+    histogram_tester.ExpectBucketCount(
+        "ServiceWorker.StaticRouter.Subresource.CORPCheckResult",
+        ServiceWorkerResourceLoader::CORPCheckResult::kViolation, 1);
+  }
+
+  // Case 3: Cross-origin request without CORP, flag ON.
+  {
+    base::test::ScopedFeatureList scoped_feature_list;
+    scoped_feature_list.InitAndEnableFeature(
+        features::kServiceWorkerStaticRouterCORPCheck);
+
+    EXPECT_FALSE(loader.IsValidStaticRouterResponse(request, response, coep,
+                                                    nullptr, dip, nullptr));
+    histogram_tester.ExpectBucketCount(
+        "ServiceWorker.StaticRouter.Subresource.CORPCheckResult",
+        ServiceWorkerResourceLoader::CORPCheckResult::kBlocked, 1);
+  }
+
+  // Case 4: Cross-origin request WITH CORP, flag ON.
+  {
+    base::test::ScopedFeatureList scoped_feature_list;
+    scoped_feature_list.InitAndEnableFeature(
+        features::kServiceWorkerStaticRouterCORPCheck);
+
+    auto corp_response = blink::mojom::FetchAPIResponse::New();
+    corp_response->response_type = network::mojom::FetchResponseType::kOpaque;
+    corp_response->headers["Cross-Origin-Resource-Policy"] = "cross-origin";
+
+    EXPECT_TRUE(loader.IsValidStaticRouterResponse(request, corp_response, coep,
+                                                   nullptr, dip, nullptr));
+    histogram_tester.ExpectBucketCount(
+        "ServiceWorker.StaticRouter.Subresource.CORPCheckResult",
+        ServiceWorkerResourceLoader::CORPCheckResult::kSuccess, 2);
+  }
+}
+
 }  // namespace content
diff --git a/content/renderer/service_worker/service_worker_provider_context_unittest.cc b/content/renderer/service_worker/service_worker_provider_context_unittest.cc
index f533cd03..69f0726 100644
--- a/content/renderer/service_worker/service_worker_provider_context_unittest.cc
+++ b/content/renderer/service_worker/service_worker_provider_context_unittest.cc
@@ -206,11 +206,8 @@
   }
   void Clone(
       mojo::PendingReceiver<blink::mojom::ControllerServiceWorker> receiver,
-      const network::CrossOriginEmbedderPolicy&,
-      mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>,
-      const network::DocumentIsolationPolicy&,
-      mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>)
-      override {
+      blink::mojom::CrossOriginEmbedderPolicyInfoPtr,
+      blink::mojom::DocumentIsolationPolicyInfoPtr) override {
     receivers_.Add(this, std::move(receiver));
   }
 
@@ -480,9 +477,7 @@
   auto controller_info1 = blink::mojom::ControllerServiceWorkerInfo::New();
   mojo::Remote<blink::mojom::ControllerServiceWorker> remote_controller1;
   fake_controller1.Clone(remote_controller1.BindNewPipeAndPassReceiver(),
-                         network::CrossOriginEmbedderPolicy(),
-                         mojo::NullRemote(), network::DocumentIsolationPolicy(),
-                         mojo::NullRemote());
+                         nullptr, nullptr);
   controller_info1->mode =
       blink::mojom::ControllerServiceWorkerMode::kControlled;
   controller_info1->fetch_handler_type =
@@ -528,9 +523,7 @@
   auto controller_info2 = blink::mojom::ControllerServiceWorkerInfo::New();
   mojo::Remote<blink::mojom::ControllerServiceWorker> remote_controller2;
   fake_controller2.Clone(remote_controller2.BindNewPipeAndPassReceiver(),
-                         network::CrossOriginEmbedderPolicy(),
-                         mojo::NullRemote(), network::DocumentIsolationPolicy(),
-                         mojo::NullRemote());
+                         nullptr, nullptr);
   controller_info2->mode =
       blink::mojom::ControllerServiceWorkerMode::kControlled;
   controller_info2->fetch_handler_type =
@@ -622,9 +615,7 @@
   auto controller_info4 = blink::mojom::ControllerServiceWorkerInfo::New();
   mojo::Remote<blink::mojom::ControllerServiceWorker> remote_controller4;
   fake_controller4.Clone(remote_controller4.BindNewPipeAndPassReceiver(),
-                         network::CrossOriginEmbedderPolicy(),
-                         mojo::NullRemote(), network::DocumentIsolationPolicy(),
-                         mojo::NullRemote());
+                         nullptr, nullptr);
   controller_info4->mode =
       blink::mojom::ControllerServiceWorkerMode::kControlled;
   controller_info4->fetch_handler_type =
@@ -781,10 +772,8 @@
   FakeControllerServiceWorker fake_controller;
   auto controller_info = blink::mojom::ControllerServiceWorkerInfo::New();
   mojo::Remote<blink::mojom::ControllerServiceWorker> remote_controller;
-  fake_controller.Clone(remote_controller.BindNewPipeAndPassReceiver(),
-                        network::CrossOriginEmbedderPolicy(),
-                        mojo::NullRemote(), network::DocumentIsolationPolicy(),
-                        mojo::NullRemote());
+  fake_controller.Clone(remote_controller.BindNewPipeAndPassReceiver(), nullptr,
+                        nullptr);
   controller_info->mode =
       blink::mojom::ControllerServiceWorkerMode::kControlled;
   controller_info->fetch_handler_type =
@@ -832,10 +821,8 @@
   FakeControllerServiceWorker fake_controller;
   auto controller_info = blink::mojom::ControllerServiceWorkerInfo::New();
   mojo::Remote<blink::mojom::ControllerServiceWorker> remote_controller;
-  fake_controller.Clone(remote_controller.BindNewPipeAndPassReceiver(),
-                        network::CrossOriginEmbedderPolicy(),
-                        mojo::NullRemote(), network::DocumentIsolationPolicy(),
-                        mojo::NullRemote());
+  fake_controller.Clone(remote_controller.BindNewPipeAndPassReceiver(), nullptr,
+                        nullptr);
   controller_info->mode =
       blink::mojom::ControllerServiceWorkerMode::kControlled;
   controller_info->fetch_handler_type =
diff --git a/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc b/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
index 1322b19..eb7d7f0 100644
--- a/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
+++ b/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
@@ -382,11 +382,8 @@
 
   void Clone(
       mojo::PendingReceiver<blink::mojom::ControllerServiceWorker> receiver,
-      const network::CrossOriginEmbedderPolicy&,
-      mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>,
-      const network::DocumentIsolationPolicy&,
-      mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>)
-      override {
+      blink::mojom::CrossOriginEmbedderPolicyInfoPtr,
+      blink::mojom::DocumentIsolationPolicyInfoPtr) override {
     receivers_.Add(this, std::move(receiver));
   }
 
@@ -493,10 +490,7 @@
     get_controller_service_worker_count_++;
     if (!fake_controller_)
       return;
-    fake_controller_->Clone(
-        std::move(receiver), network::CrossOriginEmbedderPolicy(),
-        mojo::NullRemote(), network::DocumentIsolationPolicy(),
-        mojo::NullRemote());
+    fake_controller_->Clone(std::move(receiver), nullptr, nullptr);
   }
   void CloneContainerHost(
       mojo::PendingReceiver<blink::mojom::ServiceWorkerContainerHost> receiver)
@@ -575,7 +569,11 @@
           mojo::NullRemote() /*remote_controller*/,
           mojo::NullRemote() /*remote_cache_storage*/, "" /*client_id*/,
           blink::mojom::ServiceWorkerFetchHandlerBypassOption::kDefault,
-          std::nullopt, blink::EmbeddedWorkerStatus::kStopped,
+          std::nullopt, network::CrossOriginEmbedderPolicy(),
+          mojo::NullRemote() /*cross_origin_embedder_policy_reporter*/,
+          network::DocumentIsolationPolicy(),
+          mojo::NullRemote() /*document_isolation_policy_reporter*/,
+          blink::EmbeddedWorkerStatus::kStopped,
           mojo::NullReceiver() /*running_status_receiver*/);
     }
     mojo::Remote<network::mojom::URLLoaderFactory>
@@ -1078,8 +1076,12 @@
       std::move(remote_container_host),
       mojo::NullRemote() /*remote_controller*/,
       mojo::NullRemote() /*remote_cache_storage*/, "" /*client_id*/,
-      blink::mojom::ServiceWorkerFetchHandlerBypassOption::kDefault,
-      rules, blink::EmbeddedWorkerStatus::kStopped,
+      blink::mojom::ServiceWorkerFetchHandlerBypassOption::kDefault, rules,
+      network::CrossOriginEmbedderPolicy(),
+      mojo::NullRemote() /*cross_origin_embedder_policy_reporter*/,
+      network::DocumentIsolationPolicy(),
+      mojo::NullRemote() /*document_isolation_policy_reporter*/,
+      blink::EmbeddedWorkerStatus::kRunning,
       mojo::NullReceiver() /*running_status_receiver*/);
 
   // Initialize by calling CreateSubresourceLoaderFactory() once.
diff --git a/third_party/blink/renderer/modules/service_worker/web_embedded_worker_impl_test.cc b/third_party/blink/renderer/modules/service_worker/web_embedded_worker_impl_test.cc
index 5ff7044..22ee365 100644
--- a/third_party/blink/renderer/modules/service_worker/web_embedded_worker_impl_test.cc
+++ b/third_party/blink/renderer/modules/service_worker/web_embedded_worker_impl_test.cc
@@ -399,6 +399,7 @@
   void WorkerContextStarted(
       WebServiceWorkerContextProxy* proxy,
       scoped_refptr<base::SequencedTaskRunner> worker_task_runner) override {
+    proxy_ = proxy;
     worker_task_runner_ = std::move(worker_task_runner);
     mojo::PendingAssociatedRemote<mojom::blink::ServiceWorkerHost> host_remote;
     auto host_receiver = host_remote.InitWithNewEndpointAndPassReceiver();
@@ -458,18 +459,6 @@
         mock_policy_container_host.BindNewEndpointAndPassDedicatedRemote();
     web_policy_container_ = nullptr;
 
-    // ControllerServiceWorker requires Clone to ensure
-    // CrossOriginResourcePolicyChecker. See
-    // ServiceWorkerGlobalScope::DispatchFetchEventForSubresource().
-    mojo::Remote<mojom::blink::ControllerServiceWorker>
-        stub_controller_service_worker;
-    proxy->BindControllerServiceWorker(
-        stub_controller_service_worker.BindNewPipeAndPassReceiver());
-    stub_controller_service_worker->Clone(
-        controller_service_worker_.BindNewPipeAndPassReceiver(),
-        network::CrossOriginEmbedderPolicy(), mojo::NullRemote(),
-        network::DocumentIsolationPolicy(), mojo::NullRemote());
-
     // To make the other side callable.
     host_receiver.EnableUnassociatedUsage();
     associated_interfaces_recevier_from_browser.EnableUnassociatedUsage();
@@ -534,6 +523,17 @@
     test_data_uploader_ = std::make_unique<TestDataUploader>(upload_contents);
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

CORP bypass via Service Worker Static Routing cache source

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The Service Worker Static Routing API enforces CORP checks using the Service Worker’s COEP instead of the client’s COEP when fetching from the cache source. This allows a cross-origin isolated page to load opaque cross-origin responses that lack CORP headers, bypassing security boundaries.

Affected files:

  • content/browser/service_worker/service_worker_container_host.cc
  • content/browser/service_worker/service_worker_version.cc
  • content/renderer/service_worker/service_worker_subresource_loader.cc
  • content/browser/cache_storage/cache_storage_dispatcher_host.cc

Estimated timestamp from git blame: 2025-01-31

Description

A potential security vulnerability exists in the Service Worker Static Routing API. When a Service Worker registers a routing rule with source: 'cache', the CacheStorage Mojo interface is incorrectly bound using the Service Worker’s own security policies (COEP and DIP) rather than the policies of the controlled client document.

This allows a cross-origin isolated page (COEP: require-corp) controlled by a Service Worker with a weaker policy (e.g., COEP: unsafe-none) to bypass Cross-Origin Resource Policy (CORP) checks. An attacker can cache an opaque cross-origin response without a CORP header, and then use the static routing API to load it into the cross-origin isolated renderer process, where it could be leaked using Spectre side-channels (leveraging SharedArrayBuffer and high-resolution timers).

Vulnerability Details

  1. When a navigation is committed for a controlled client, ServiceWorkerContainerHostForClient::CreateControllerServiceWorkerInfo() is called. If the Service Worker has a static router evaluator, it calls controller()->GetRemoteCacheStorage() to provide the renderer with direct access to the cache.
  2. In ServiceWorkerVersion::GetRemoteCacheStorage(), the CacheStorage receiver is bound using the Service Worker’s own cross_origin_embedder_policy() and document_isolation_policy(). The Mojo remote is then sent to the client renderer.
  3. When the client renderer initiates a subresource fetch that matches a source: 'cache' rule, ServiceWorkerSubresourceLoader calls Match() on the CacheStorage remote.
  4. In the browser process, CacheStorageDispatcherHost::CacheStorageImpl::Match enforces CORP by calling ResponseBlockedByCrossOriginResourcePolicy(). However, it uses the COEP that was bound to the receiver (the Service Worker’s unsafe-none policy) instead of the client’s require-corp policy.
  5. Consequently, the opaque cross-origin response is returned to the client renderer without being blocked by CORP, violating the security guarantees of the cross-origin isolated page.

Suggested Reproduction Steps

Note: These steps are suggested based on code analysis; a working proof of concept has not yet been executed.

  1. Create a Service Worker script served without a Cross-Origin-Embedder-Policy header (resulting in COEP: unsafe-none).
  2. In the Service Worker’s install event, use the Service Worker Static Routing API to register a route matching a specific cross-origin URL pattern with a source: 'cache' rule (event.addRoutes([{ condition: { urlPattern: '...' }, source: 'cache' }])).
  3. Populate the Service Worker’s cache with an opaque cross-origin response from the target URL (e.g., using cache.put() on a no-cors fetch). Ensure the target response lacks a CORP header.
  4. Host a client page controlled by this Service Worker that is cross-origin isolated (served with COEP: require-corp and Cross-Origin-Opener-Policy: same-origin).
  5. Navigate the browser to the client page.
  6. From the client page, initiate a no-cors fetch for the target cross-origin URL.
  7. The browser returns the opaque response from the cache because the CORP check is incorrectly evaluated against the Service Worker’s unsafe-none policy.
  8. The opaque response enters the cross-origin isolated renderer process, where its contents could be read via Spectre side-channels.

Suggested Fix

Ensure that the CacheStorage Mojo remote provided to the client for static routing is bound using the client’s own COEP and DIP, rather than the Service Worker’s policies. When ServiceWorkerContainerHostForClient prepares the controller info, it should request a cache storage remote specifically configured for the client’s security context.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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