CVE-2026-14035
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/bluetooth/web_bluetooth_service_impl.cc |
modified |
Files Changed
content/browser/bluetooth/web_bluetooth_service_impl.cccontent/browser/bluetooth/web_bluetooth_service_impl.hcontent/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
Patch
From e6b4397484c484f790d1ba2c75fd1c0b59ab6182 Mon Sep 17 00:00:00 2001 From: Rob Pitkin <[email protected]> Date: Tue, 05 May 2026 16:12:32 -0700 Subject: [PATCH] Web Bluetooth: Abort pending watchAdvertisements on visibility loss When a Web Bluetooth client calls watchAdvertisements(), if no active discovery session exists, the request is placed in the watch_advertisements_pending_clients_ queue. If the page loses visibility, ClearAdvertisementClients() is called. However, this function only cleared active clients and neglected watch_advertisements_pending_clients_. This CL ensures that all pending clients are aborted and cleared from watch_advertisements_pending_clients_ during ClearAdvertisementClients(), preventing visibility bypass. Added a test to verify that pending requests are aborted when tab is hidden. Bug: 496371586 Change-Id: I98945181d9eb132a4cec1869edfdb20ca269a2c9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7814024 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Rob Pitkin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1625796} --- diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc index 534f529..9a8a4ff1 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl.cc +++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc @@ -2391,6 +2391,13 @@ void WebBluetoothServiceImpl::ClearAdvertisementClients() { scanning_clients_.clear(); watch_advertisements_clients_.clear(); + + auto pending_clients = std::move(watch_advertisements_pending_clients_); + for (auto& pending_client : pending_clients) { + pending_client->RunCallback( + blink::mojom::WebBluetoothResult::WATCH_ADVERTISEMENTS_ABORTED); + } + allowed_scan_filters_.clear(); accept_all_advertisements_ = false; } diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.h b/content/browser/bluetooth/web_bluetooth_service_impl.h index 2b37b966..3798fb0 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl.h +++ b/content/browser/bluetooth/web_bluetooth_service_impl.h @@ -165,6 +165,8 @@ FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, TwoWatchAdvertisementsReqFail); FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, + WatchAdvertisementsReqAbortedWhenTabHidden); + FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, SecWatchAdvertisementsReqAfterFirstSuccess); FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, WatchAdvertisementsFastPathChecksPermission); diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc index 5edbae8..309c147 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc +++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc @@ -1161,6 +1161,30 @@ } TEST_F(WebBluetoothServiceImplTest, + WatchAdvertisementsReqAbortedWhenTabHidden) { + TestFuture<WebBluetoothResult> future; + + const auto battery_device_id = AddTestDevice(battery_device_bundle()); + + mojo::PendingAssociatedRemote<blink::mojom::WebBluetoothAdvertisementClient> + client_remote; + + battery_device_bundle().advertisement_client().BindReceiver( + client_remote.InitWithNewEndpointAndPassReceiver()); + + // Install SUCCESS result for StartScanWithFilter + adapter_->SetStartScanWithFilterResult( + device::UMABluetoothDiscoverySessionOutcome::SUCCESS); + + service_ptr_->WatchAdvertisementsForDevice( + battery_device_id, std::move(client_remote), future.GetCallback()); + + contents()->SetVisibilityAndNotifyObservers(Visibility::HIDDEN); + + EXPECT_EQ(future.Get(), WebBluetoothResult::WATCH_ADVERTISEMENTS_ABORTED); +} + +TEST_F(WebBluetoothServiceImplTest, SecWatchAdvertisementsReqAfterFirstSuccess) { // Install SUCCESS result for StartScanWithFilter adapter_->SetStartScanWithFilterResult(
Regression Test / PoC
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
index 5edbae8..309c147 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -1161,6 +1161,30 @@
}
TEST_F(WebBluetoothServiceImplTest,
+ WatchAdvertisementsReqAbortedWhenTabHidden) {
+ TestFuture<WebBluetoothResult> future;
+
+ const auto battery_device_id = AddTestDevice(battery_device_bundle());
+
+ mojo::PendingAssociatedRemote<blink::mojom::WebBluetoothAdvertisementClient>
+ client_remote;
+
+ battery_device_bundle().advertisement_client().BindReceiver(
+ client_remote.InitWithNewEndpointAndPassReceiver());
+
+ // Install SUCCESS result for StartScanWithFilter
+ adapter_->SetStartScanWithFilterResult(
+ device::UMABluetoothDiscoverySessionOutcome::SUCCESS);
+
+ service_ptr_->WatchAdvertisementsForDevice(
+ battery_device_id, std::move(client_remote), future.GetCallback());
+
+ contents()->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
+
+ EXPECT_EQ(future.Get(), WebBluetoothResult::WATCH_ADVERTISEMENTS_ABORTED);
+}
+
+TEST_F(WebBluetoothServiceImplTest,
SecWatchAdvertisementsReqAfterFirstSuccess) {
// Install SUCCESS result for StartScanWithFilter
adapter_->SetStartScanWithFilterResult(
Original Bug Report
Potential Web Bluetooth watchAdvertisements visibility bypass via uncleared pending-client queue
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The Web Bluetooth implementation in Chrome fails to properly clear pending client requests and discovery session state when a tab is hidden. This allows a backgrounded tab to potentially continue receiving Bluetooth advertisement data from authorized devices, bypassing intended privacy protections. Two distinct bypass vectors exist due to incomplete state clearing in ClearAdvertisementClients().
Affected files:
content/browser/bluetooth/web_bluetooth_service_impl.cccontent/browser/bluetooth/web_bluetooth_service_impl.hcontent/browser/bluetooth/advertisement_client.cc
Estimated timestamp from git blame: 2026-03-11
Description
A potential privacy bypass vulnerability exists in the Web Bluetooth API implementation in Chrome. When a tab is backgrounded or loses focus, WebBluetoothServiceImpl::ClearAdvertisementClients() is called to stop the delivery of Bluetooth advertisements to the renderer. However, this function is incomplete, leading to two potential bypass vectors that could allow hidden tabs to receive proximity and tracking data (RSSI, manufacturer data, etc.) from previously authorized Bluetooth devices.
1. Pending Client Promotion Race
When device.watchAdvertisements() is called and no discovery session is currently active, the client is added to a pending queue (watch_advertisements_pending_clients_) and an asynchronous request to start a discovery session is sent to the platform.
If the tab is hidden before the platform responds, ClearAdvertisementClients() is triggered but fails to clear this pending queue (content/browser/bluetooth/web_bluetooth_service_impl.cc:2382). When OnStartDiscoverySessionForWatchAdvertisements() eventually receives the session handle, it iterates through the surviving pending queue and moves the clients into the active watch_advertisements_clients_ list (content/browser/bluetooth/web_bluetooth_service_impl.cc:1788). The checks performed during this promotion only verify device permission and Mojo pipe liveness, not the current visibility state of the tab, allowing a hidden tab to become an active listener.
2. Synchronous Session Reuse Bypass
ClearAdvertisementClients() also fails to reset the watch_advertisements_discovery_session_ handle or call MaybeStopDiscovery(). Consequently, the discovery session remains active in the browser process even after a tab is backgrounded.
If a hidden tab re-sends the watchAdvertisements IPC, WatchAdvertisementsForDeviceImpl() follows a synchronous fast-path (lines 1733-1741 in web_bluetooth_service_impl.cc) because the session already exists. This path directly inserts the client into the active list without any visibility checks, synchronously re-subscribing the hidden tab to the active session.
Once a client is in the active list via either vector, DeviceAdvertisementReceived() delivers advertisement events to the hidden tab via WatchAdvertisementsClient::SendEvent (content/browser/bluetooth/advertisement_client.cc:62). This enables persistent tracking of the user’s nearby Bluetooth devices without their knowledge or consent, even when the tab is not visible.
(Note: These are potential steps and analysis, as this agent cannot run code to verify.)
Suggested Steps to Reproduce
Vector 1 (Race):
- Grant an origin permission to a Bluetooth device via
navigator.bluetooth.requestDevice(). - From the page, call
device.watchAdvertisements(). - Immediately switch to another tab or minimize the browser window before the OS Bluetooth session starts.
- Observe that the hidden page potentially continues to receive
advertisingeventdata (RSSI, manufacturer data) despite the visibility change.
Vector 2 (Synchronous Reuse):
- Grant an origin permission to a Bluetooth device.
- From the page, call
device.watchAdvertisements()and wait for the session to successfully start. - Switch to another tab or minimize the browser window.
- While hidden, have the page call
device.watchAdvertisements()again. - Observe that the hidden page potentially receives
advertisingeventdata.
Suggested Fix
Update WebBluetoothServiceImpl::ClearAdvertisementClients() to comprehensively clear all advertisement-related state:
- Clear the
watch_advertisements_pending_clients_queue. - Reset
watch_advertisements_discovery_session_andble_scan_discovery_session_. - Alternatively, ensure
MaybeStopDiscovery()is called when visibility changes so that the underlying platform sessions are properly halted when no active visible clients remain.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results 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. Please feel free to reach out to me if you have concerns or feedback.