Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in ServiceWorker
DescriptionMissing authorization in ServiceWorker
ComponentServiceWorker
Bug ClassLogic Error
Tracker497025031
Fix commit776cc7a4cf94 (chromium/src) +179/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

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

Files Changed

  • content/browser/service_worker/service_worker_metrics.h
  • content/browser/service_worker/service_worker_version.cc
  • content/browser/service_worker/service_worker_version.h
  • content/browser/service_worker/service_worker_version_unittest.cc
From 776cc7a4cf946dcfe0ae9aece0d3fd9805568774 Mon Sep 17 00:00:00 2001
From: Tsuyoshi Horo <[email protected]>
Date: Tue, 04 Aug 2026 20:06:52 -0700
Subject: [PATCH] [ServiceWorker] Verify pending click event in OpenNewTab and FocusClient

Before this change, ServiceWorkerVersion::OpenNewTab and FocusClient did
not check that the service worker requesting to open or focus a window
was actually handling an event that grants window interaction.

This CL adds a check in OpenNewTab and FocusClient to ensure there is an
active, pending NOTIFICATION_CLICK, PAYMENT_REQUEST or
BACKGROUND_FETCH_CLICK event before opening or focusing a window. If
none is found, the renderer is killed. This mirrors the existing check
in OpenPaymentHandlerWindow and matches the set of events for which the
renderer grants the window-interaction token.

Fixed: 497025031
Change-Id: Ia75d781d5e024ab35b6eac0b6561431785216458
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8196685
Commit-Queue: Tsuyoshi Horo <[email protected]>
Reviewed-by: Shunya Shishido <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1673867}
---

diff --git a/content/browser/service_worker/service_worker_metrics.h b/content/browser/service_worker/service_worker_metrics.h
index 2865817..dab59397 100644
--- a/content/browser/service_worker/service_worker_metrics.h
+++ b/content/browser/service_worker/service_worker_metrics.h
@@ -84,6 +84,9 @@
   };
 
   // Used for UMA. Append-only.
+  // Note: This enum is also used for behavioral checks, such as verifying
+  // if a service worker has a pending event that allows window interaction.
+  //
   // This class is used to indicate which event is fired/finished. Most events
   // have only one request that starts the event and one response that finishes
   // the event, but the fetch event has two responses, so there are two types of
diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index 478baa3bd..79fc6556 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -1874,6 +1874,13 @@
     receiver_.reset();
     return;
   }
+  if (!HasPendingWindowInteractionEvent()) {
+    associated_interface_receiver_.ReportBadMessage(
+        "Received Clients#openWindow() request without a pending event that "
+        "allows window interaction.");
+    receiver_.reset();
+    return;
+  }
   // TODO(crbug.com/40177656): After StorageKey implements partitioning update
   // this to reject with InvalidAccessError if key_ is partitioned.
   OpenWindow(url, service_worker_client_utils::WindowType::NEW_TAB_WINDOW,
@@ -1973,6 +1980,55 @@
   return false;
 }
 
+bool ServiceWorkerVersion::HasPendingWindowInteractionEvent() {
+  // Despite using a const_iterator, this method cannot be const because
+  // base::IDMap::Iterator always modifies the Map object it is iterating
+  // over (to update bookkeeping state).
+  base::IDMap<std::unique_ptr<InflightRequest>>::const_iterator iter(
+      &inflight_requests_);
+  while (!iter.IsAtEnd()) {
+    switch (iter.GetCurrentValue()->event_type) {
+      case ServiceWorkerMetrics::EventType::NOTIFICATION_CLICK:
+      case ServiceWorkerMetrics::EventType::PAYMENT_REQUEST:
+      case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_CLICK:
+        return true;
+      case ServiceWorkerMetrics::EventType::ACTIVATE:
+      case ServiceWorkerMetrics::EventType::INSTALL:
+      case ServiceWorkerMetrics::EventType::SYNC:
+      case ServiceWorkerMetrics::EventType::PUSH:
+      case ServiceWorkerMetrics::EventType::MESSAGE:
+      case ServiceWorkerMetrics::EventType::NOTIFICATION_CLOSE:
+      case ServiceWorkerMetrics::EventType::FETCH_MAIN_FRAME:
+      case ServiceWorkerMetrics::EventType::FETCH_SUB_FRAME:
+      case ServiceWorkerMetrics::EventType::FETCH_SHARED_WORKER:
+      case ServiceWorkerMetrics::EventType::FETCH_SUB_RESOURCE:
+      case ServiceWorkerMetrics::EventType::UNKNOWN:
+      case ServiceWorkerMetrics::EventType::FETCH_WAITUNTIL:
+      case ServiceWorkerMetrics::EventType::EXTERNAL_REQUEST:
+      case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_ABORT:
+      case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_FAIL:
+      case ServiceWorkerMetrics::EventType::NAVIGATION_HINT:
+      case ServiceWorkerMetrics::EventType::CAN_MAKE_PAYMENT:
+      case ServiceWorkerMetrics::EventType::ABORT_PAYMENT:
+      case ServiceWorkerMetrics::EventType::COOKIE_CHANGE:
+      case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_SUCCESS:
+      case ServiceWorkerMetrics::EventType::PERIODIC_SYNC:
+      case ServiceWorkerMetrics::EventType::CONTENT_DELETE:
+      case ServiceWorkerMetrics::EventType::PUSH_SUBSCRIPTION_CHANGE:
+      case ServiceWorkerMetrics::EventType::FETCH_FENCED_FRAME:
+      case ServiceWorkerMetrics::EventType::BYPASS_MAIN_RESOURCE:
+      case ServiceWorkerMetrics::EventType::SKIP_EMPTY_FETCH_HANDLER:
+      case ServiceWorkerMetrics::EventType::
+          BYPASS_ONLY_IF_SERVICE_WORKER_NOT_STARTED:
+      case ServiceWorkerMetrics::EventType::WARM_UP:
+      case ServiceWorkerMetrics::EventType::STATIC_ROUTER:
+        break;
+    }
+    iter.Advance();
+  }
+  return false;
+}
+
 void ServiceWorkerVersion::DidShowPaymentHandlerWindow(
     const GURL& url,
     const blink::StorageKey& key,
@@ -2105,6 +2161,13 @@
     receiver_.reset();
     return;
   }
+  if (!HasPendingWindowInteractionEvent()) {
+    associated_interface_receiver_.ReportBadMessage(
+        "Received WindowClient#focus() request without a pending event that "
+        "allows window interaction.");
+    receiver_.reset();
+    return;
+  }
   ServiceWorkerClient* service_worker_client =
       context_->service_worker_client_owner().GetServiceWorkerClientByClientID(
           client_uuid);
diff --git a/content/browser/service_worker/service_worker_version.h b/content/browser/service_worker/service_worker_version.h
index 9a43abf..2261c1d 100644
--- a/content/browser/service_worker/service_worker_version.h
+++ b/content/browser/service_worker/service_worker_version.h
@@ -1146,6 +1146,10 @@
   // for the current service worker version.
   bool HasPendingPaymentRequestEvent();
 
+  // Checks if there is an active and pending event that allows the worker to
+  // open a new window or focus an existing one.
+  bool HasPendingWindowInteractionEvent();
+
   void DidShowPaymentHandlerWindow(
       const GURL& url,
       const blink::StorageKey& key,
diff --git a/content/browser/service_worker/service_worker_version_unittest.cc b/content/browser/service_worker/service_worker_version_unittest.cc
index 7e45b499..424a21e 100644
--- a/content/browser/service_worker/service_worker_version_unittest.cc
+++ b/content/browser/service_worker/service_worker_version_unittest.cc
@@ -2873,5 +2873,114 @@
       bad_message_observer.WaitForBadMessage());
 }
 
+// Verifies that OpenNewTab() rejects calls for a service worker that doesn't
+// have a pending event that allows window interaction, and kills the renderer.
+TEST_P(ServiceWorkerVersionTest, OpenNewTab_NoPendingEvent) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+
+  GURL url = scope_.Resolve("page.html");
+  base::test::TestFuture<bool, ServiceWorkerClientInfoPtr,
+                         const std::optional<std::string>&>
+      future;
+
+  service_worker->host()->OpenNewTab(url, future.GetCallback());
+
+  EXPECT_EQ(
+      "Received Clients#openWindow() request without a pending event that "
+      "allows window interaction.",
+      bad_message_observer.WaitForBadMessage());
+}
+
+// Verifies that OpenNewTab() accepts calls for a service worker that has a
+// pending NOTIFICATION_CLICK event.
+TEST_P(ServiceWorkerVersionTest, OpenNewTab_WithPendingNotificationClickEvent) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+
+  // Simulate a pending NOTIFICATION_CLICK event.
+  int request_id = version_->StartRequest(
+      ServiceWorkerMetrics::EventType::NOTIFICATION_CLICK, base::DoNothing());
+
+  GURL url = scope_.Resolve("page.html");
+  base::test::TestFuture<bool, ServiceWorkerClientInfoPtr,
+                         const std::optional<std::string>&>
+      future;
+
+  service_worker->host()->OpenNewTab(url, future.GetCallback());
+  EXPECT_TRUE(future.Wait());
+
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+
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 7e45b499..424a21e 100644
--- a/content/browser/service_worker/service_worker_version_unittest.cc
+++ b/content/browser/service_worker/service_worker_version_unittest.cc
@@ -2873,5 +2873,114 @@
       bad_message_observer.WaitForBadMessage());
 }
 
+// Verifies that OpenNewTab() rejects calls for a service worker that doesn't
+// have a pending event that allows window interaction, and kills the renderer.
+TEST_P(ServiceWorkerVersionTest, OpenNewTab_NoPendingEvent) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+
+  GURL url = scope_.Resolve("page.html");
+  base::test::TestFuture<bool, ServiceWorkerClientInfoPtr,
+                         const std::optional<std::string>&>
+      future;
+
+  service_worker->host()->OpenNewTab(url, future.GetCallback());
+
+  EXPECT_EQ(
+      "Received Clients#openWindow() request without a pending event that "
+      "allows window interaction.",
+      bad_message_observer.WaitForBadMessage());
+}
+
+// Verifies that OpenNewTab() accepts calls for a service worker that has a
+// pending NOTIFICATION_CLICK event.
+TEST_P(ServiceWorkerVersionTest, OpenNewTab_WithPendingNotificationClickEvent) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+
+  // Simulate a pending NOTIFICATION_CLICK event.
+  int request_id = version_->StartRequest(
+      ServiceWorkerMetrics::EventType::NOTIFICATION_CLICK, base::DoNothing());
+
+  GURL url = scope_.Resolve("page.html");
+  base::test::TestFuture<bool, ServiceWorkerClientInfoPtr,
+                         const std::optional<std::string>&>
+      future;
+
+  service_worker->host()->OpenNewTab(url, future.GetCallback());
+  EXPECT_TRUE(future.Wait());
+
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+
+  // Clean up the pending request.
+  version_->FinishRequest(request_id, /*was_handled=*/true);
+}
+
+// Verifies that OpenNewTab() accepts calls for a service worker that has a
+// pending BACKGROUND_FETCH_CLICK event.
+TEST_P(ServiceWorkerVersionTest,
+       OpenNewTab_WithPendingBackgroundFetchClickEvent) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+
+  // Simulate a pending BACKGROUND_FETCH_CLICK event.
+  int request_id = version_->StartRequest(
+      ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_CLICK,
+      base::DoNothing());
+
+  GURL url = scope_.Resolve("page.html");
+  base::test::TestFuture<bool, ServiceWorkerClientInfoPtr,
+                         const std::optional<std::string>&>
+      future;
+
+  service_worker->host()->OpenNewTab(url, future.GetCallback());
+  EXPECT_TRUE(future.Wait());
+
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+
+  // Clean up the pending request.
+  version_->FinishRequest(request_id, /*was_handled=*/true);
+}
+
+// Verifies that FocusClient() rejects calls for a service worker that doesn't
+// have a pending event that allows window interaction, and kills the renderer.
+TEST_P(ServiceWorkerVersionTest, FocusClient_NoPendingEvent) {
+  auto* service_worker =
+      helper_->AddNewPendingServiceWorker<FakeServiceWorker>(helper_.get());
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  service_worker->RunUntilInitializeGlobalScope();
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+
+  base::test::TestFuture<blink::mojom::FocusResultPtr> future;
+  service_worker->host()->FocusClient(
+      base::Uuid::GenerateRandomV4().AsLowercaseString(), future.GetCallback());
+
+  EXPECT_EQ(
+      "Received WindowClient#focus() request without a pending event that "
+      "allows window interaction.",
+      bad_message_observer.WaitForBadMessage());
+}
+
 }  // namespace service_worker_version_unittest
 }  // namespace content
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.