Chrome · ServiceWorker
CVE-2026-87429
Logic Error in ServiceWorker
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcontent/browser/service_worker/service_worker_version_unittest.cc |
modified |
Files Changed
content/browser/service_worker/service_worker_metrics.hcontent/browser/service_worker/service_worker_version.cccontent/browser/service_worker/service_worker_version.hcontent/browser/service_worker/service_worker_version_unittest.cc
Patch
From 776cc7a4cf946dcfe0ae9aece0d3fd9805568774 Mon Sep 17 00:00:00 2001
From: Tsuyoshi Horo <horo@chromium.org>
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 <horo@chromium.org>
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
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.
References
On This Page