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 <[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.
References
On This Page