Low watch CVSS 4.3 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in ServiceWorker
DescriptionIncorrect authorization in ServiceWorker
ComponentServiceWorker
Sub-componentServiceWorker
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker499218516
Fix commit9ac349c94b98 (chromium/src) +48/-11
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-03 (per vendor advisory)
Fix landed2026-08-03 (public git)
Disclosed2026-09-08
Patch gap36 days — fix public in source before the advisory

Background

ServiceWorker client
a document, worker, or worklet execution context (ServiceWorkerClient) that a service worker can enumerate and interact with, identified by an opaque client_uuid.
`blink::StorageKey`
the browser’s storage-partitioning identity for a context, combining the origin, the top-level site (net::SchemefulSite), and an AncestorChainBit, so two same-origin contexts in different partitions have different keys.
By-UUID client operations
the ServiceWorkerVersion methods (GetClient, PostMessageToClient, FocusClient, NavigateClient) that resolve a client from a UUID supplied by the service worker script and then act on it.
`GetUrlForScopeMatch()` / `DeprecatedGetOriginAsURL()`
helpers that yield a client’s scope-match URL and reduce a URL to its bare origin, used by the pre-patch authorization check.

Root Cause Analysis

The by-UUID operations in service_worker_version.cc authorized access to a target client by comparing only the client’s URL-derived origin (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL()) against the worker’s script origin (script_url_.DeprecatedGetOriginAsURL()). This origin-only comparison ignores the rest of the storage-partitioning identity, so a same-origin client living in a different storage partition — a third-party-partitioned iframe or a fenced frame — satisfied the check even though GetClients (Clients#matchAll) already excluded it by filtering on the full blink::StorageKey. The invariant violated is that a service worker must only reach clients within its own storage partition (key_), not merely its own origin.

The fix replaces every DeprecatedGetOriginAsURL() comparison with service_worker_client->key() != key_, comparing the client’s full StorageKey against the version’s key, and adds the same key() check to the by-UUID lookup that previously gated only on is_execution_ready(). This tightens the check so partition-mismatched same-origin clients are treated as absent (GetClient returns nullptr/undefined) or rejected (ReportBadMessage), matching the behavior of matchAll.

Key insight
The single mistake was authorizing cross-partition access with an origin-only equality test when the correct authorization boundary is the full blink::StorageKey; the fix aligns the by-UUID operations with matchAll by comparing service_worker_client->key() to the version’s key_.

Attack Path

  1. Set up a partitioned same-origin client An attacker embeds a same-origin (origin A) iframe or fenced frame inside a cross-site top-level page, so the client’s StorageKey shares origin A but has a different top-level site / AncestorChainBit than the service worker’s key_.
  2. Obtain the client UUID A service worker for origin A obtains the target client’s client_uuid; because the UUID comes from a Client object, the by-UUID path trusts it as valid.
  3. Invoke a by-UUID operation The worker calls Clients.get(), client.postMessage(), WindowClient.focus(), or WindowClient.navigate() against that UUID; pre-patch, the origin-only check passed and the operation proceeded on the partition-mismatched client.
  4. Cross-partition interaction The worker reads client info, delivers messages, focuses, or navigates a same-origin context in a different storage partition, defeating storage partitioning isolation that matchAll otherwise enforces.

Impact Assessment

An attacker-controlled service worker gains the ability to enumerate/read, message, focus, or navigate same-origin clients that reside in a different storage partition (third-party-partitioned iframe or fenced frame), breaking the cross-partition isolation guarantee; the authorization decision is made in the browser process while the operations are requested by the compromised/attacker renderer. Preconditions are that the target is same-origin with the worker but in a distinct StorageKey partition and that the worker holds the target’s client_uuid. The advisory classifies this as incorrect authorization in ServiceWorker; no exploited-in-the-wild status is stated, and any escalation beyond cross-partition client access is not demonstrated by the patch (inferred).

Changed Functions

FunctionChangeNotes
TEST_P
content/browser/service_worker/service_worker_version_unittest.cc
modified

Files Changed

  • content/browser/service_worker/service_worker_version.cc
  • content/browser/service_worker/service_worker_version_unittest.cc

Audit Directions

  • Origin-vs-StorageKey mismatches
    Search for remaining DeprecatedGetOriginAsURL() or bare origin equality checks used as authorization boundaries where the surrounding subsystem is partitioned by blink::StorageKey.
  • Client/context lookups by caller-supplied ID
    Audit other by-UUID or by-token lookups (documents, workers, fenced frames) to confirm they compare the full partitioning identity rather than only origin or scope.
  • Consistency across enumeration and direct-access APIs
    Where a “list/matchAll” API filters by one predicate, verify the corresponding “get/act-by-id” APIs apply the identical predicate, since divergence is the exact defect here.
From 9ac349c94b98903e8bbc2667eddd5b067fabdb9f Mon Sep 17 00:00:00 2001
From: Hayato Ito <hayato@chromium.org>
Date: Sun, 02 Aug 2026 22:55:07 -0700
Subject: [PATCH] [ServiceWorker] Filter by-UUID client operations by StorageKey

ServiceWorkerVersion::GetClient/PostMessageToClient/FocusClient/NavigateClient
compared the target client's URL-derived origin against the script URL's
origin, while GetClients (Clients#matchAll) filters by full
blink::StorageKey via IsSameOriginServiceWorkerClient. Make the by-UUID
operations consistent by comparing the client's StorageKey against the
version's key_, so that same-origin clients in a different storage
partition (third-party-partitioned iframe / fenced frame) are not
reachable. This also removes the last uses of DeprecatedGetOriginAsURL()
in this file.

Bug: 499218516
Change-Id: Iffa288dad2f75bfc3e6152ffdc604cfa90ac69f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8177241
Commit-Queue: Hayato Ito <hayato@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672504}
---
diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index e8d3e3f7..35ca907 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -1825,12 +1825,10 @@
   ServiceWorkerClient* service_worker_client =
       context_->service_worker_client_owner().GetServiceWorkerClientByClientID(
           client_uuid);
-  if (!service_worker_client ||
-      service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
-          script_url_.DeprecatedGetOriginAsURL()) {
+  if (!service_worker_client || service_worker_client->key() != key_) {
     // The promise will be resolved to 'undefined'.
     // Note that we don't BadMessage here since Clients#get() can be passed an
-    // arbitrary UUID. The BadMessages for the origin mismatches below are
+    // arbitrary UUID. The BadMessages for the storage key mismatches below are
     // appropriate because the UUID is taken directly from a Client object so we
     // expect it to be valid.
     std::move(callback).Run(nullptr);
@@ -1857,7 +1855,8 @@
   ServiceWorkerClient* service_worker_client =
       context_->service_worker_client_owner().GetServiceWorkerClientByClientID(
           client_uuid);
-  if (!service_worker_client || !service_worker_client->is_execution_ready()) {
+  if (!service_worker_client || service_worker_client->key() != key_ ||
+      !service_worker_client->is_execution_ready()) {
     std::move(callback).Run(nullptr);
     return;
   }
@@ -2026,8 +2025,7 @@
     }
   }
 
-  if (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
-      script_url_.DeprecatedGetOriginAsURL()) {
+  if (service_worker_client->key() != key_) {
     associated_interface_receiver_.ReportBadMessage(
         "Received Client#postMessage() request for a cross-origin client.");
     receiver_.reset();
@@ -2086,8 +2084,7 @@
     std::move(callback).Run(std::move(result));
     return;
   }
-  if (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
-      script_url_.DeprecatedGetOriginAsURL()) {
+  if (service_worker_client->key() != key_) {
     associated_interface_receiver_.ReportBadMessage(
         "Received WindowClient#focus() request for a cross-origin client.");
     receiver_.reset();
@@ -2144,8 +2141,7 @@
                             std::string("The client was not found."));
     return;
   }
-  if (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
-      script_url_.DeprecatedGetOriginAsURL()) {
+  if (service_worker_client->key() != key_) {
     associated_interface_receiver_.ReportBadMessage(
         "Received WindowClient#navigate() request for a cross-origin client.");
     receiver_.reset();
diff --git a/content/browser/service_worker/service_worker_version_unittest.cc b/content/browser/service_worker/service_worker_version_unittest.cc
index 51b7b0f0..b965e009 100644
--- a/content/browser/service_worker/service_worker_version_unittest.cc
+++ b/content/browser/service_worker/service_worker_version_unittest.cc
@@ -52,6 +52,7 @@
 #include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
 #include "third_party/blink/public/common/service_worker/service_worker_router_rule.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_event_status.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_installed_scripts_manager.mojom.h"
@@ -1517,6 +1518,46 @@
             StartServiceWorker(version.get()));
 }
 
+// Test that GetClient() returns null when requested for a client with the same
+// origin but a different storage key.
+TEST_P(ServiceWorkerVersionTest, GetClientWithDifferentStorageKey) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  ScopedServiceWorkerClient service_worker_client =
+      helper_->context()
+          ->service_worker_client_owner()
+          .CreateServiceWorkerClientForWorker(
+              helper_->mock_render_process_id(),
+              ServiceWorkerClientInfo(blink::SharedWorkerToken()));
+
+  GURL client_url = scope_.Resolve("shared_worker.js");
+  auto client_origin = url::Origin::Create(client_url);
+  auto different_top_level_origin =
+      url::Origin::Create(GURL("https://www.different.com/"));
+  auto different_storage_key = blink::StorageKey::Create(
+      client_origin, net::SchemefulSite(different_top_level_origin),
+      blink::mojom::AncestorChainBit::kCrossSite,
+      /*third_party_partitioning_allowed=*/true);
+
+  service_worker_client->UpdateUrls(client_url, different_top_level_origin,
+                                    different_storage_key);
+
+  CommittedServiceWorkerClient committed_client(
+      std::move(service_worker_client));
+  committed_client->SetExecutionReady();
+
+  base::test::TestFuture<blink::mojom::ServiceWorkerClientInfoPtr> future;
+  service_worker->host()->GetClient(committed_client->client_uuid(),
+                                    future.GetCallback());
+
+  EXPECT_TRUE(future.Get().is_null());
+}
+
 TEST_P(ServiceWorkerVersionTest,
        StartWorker_ContentSettingsDisallowsServiceWorker_FeatureEnabled) {
   base::test::ScopedFeatureList feature_list;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/service_worker/service_worker_version_unittest.cc b/content/browser/service_worker/service_worker_version_unittest.cc
index 51b7b0f0..b965e009 100644
--- a/content/browser/service_worker/service_worker_version_unittest.cc
+++ b/content/browser/service_worker/service_worker_version_unittest.cc
@@ -52,6 +52,7 @@
 #include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
 #include "third_party/blink/public/common/service_worker/service_worker_router_rule.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_event_status.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_installed_scripts_manager.mojom.h"
@@ -1517,6 +1518,46 @@
             StartServiceWorker(version.get()));
 }
 
+// Test that GetClient() returns null when requested for a client with the same
+// origin but a different storage key.
+TEST_P(ServiceWorkerVersionTest, GetClientWithDifferentStorageKey) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  ScopedServiceWorkerClient service_worker_client =
+      helper_->context()
+          ->service_worker_client_owner()
+          .CreateServiceWorkerClientForWorker(
+              helper_->mock_render_process_id(),
+              ServiceWorkerClientInfo(blink::SharedWorkerToken()));
+
+  GURL client_url = scope_.Resolve("shared_worker.js");
+  auto client_origin = url::Origin::Create(client_url);
+  auto different_top_level_origin =
+      url::Origin::Create(GURL("https://www.different.com/"));
+  auto different_storage_key = blink::StorageKey::Create(
+      client_origin, net::SchemefulSite(different_top_level_origin),
+      blink::mojom::AncestorChainBit::kCrossSite,
+      /*third_party_partitioning_allowed=*/true);
+
+  service_worker_client->UpdateUrls(client_url, different_top_level_origin,
+                                    different_storage_key);
+
+  CommittedServiceWorkerClient committed_client(
+      std::move(service_worker_client));
+  committed_client->SetExecutionReady();
+
+  base::test::TestFuture<blink::mojom::ServiceWorkerClientInfoPtr> future;
+  service_worker->host()->GetClient(committed_client->client_uuid(),
+                                    future.GetCallback());
+
+  EXPECT_TRUE(future.Get().is_null());
+}
+
 TEST_P(ServiceWorkerVersionTest,
        StartWorker_ContentSettingsDisallowsServiceWorker_FeatureEnabled) {
   base::test::ScopedFeatureList feature_list;
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.