CVE-2026-11236
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
erase_ifcontent/browser/bluetooth/advertisement_client.cc |
modified | |
ifcontent/browser/bluetooth/advertisement_client.cc |
modified |
Files Changed
content/browser/bluetooth/advertisement_client.ccthird_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js
Patch
From 1f60444928cc7162a6db404bf64bad98e094e7a8 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Thu, 16 Apr 2026 07:36:38 -0700 Subject: [PATCH] Web Bluetooth: Apply blocklist in requestLEScan WebBluetoothServiceImpl::ScanningClient::SendEvent failed to filter advertisement data (UUIDs, service data, and manufacturer data) against the BluetoothBlocklist. This allowed restricted data, such as iBeacon proximity UUIDs, to be sent to the renderer. This CL adds blocklist filtering to ScanningClient::SendEvent, mirroring the protection already present in WatchAdvertisementsClient::SendEvent. A new web test is added to verify that blocklisted manufacturer data is correctly stripped from advertisement events during a LE scan. https://github.com/WebBluetoothCG/web-bluetooth/issues/669 Fixed: 496427030 Change-Id: Ia17c9fe9b81f3dca25a2f7f5b2f3d7147e57f3e1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747414 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Cr-Commit-Position: refs/heads/main@{#1615840} --- diff --git a/content/browser/bluetooth/advertisement_client.cc b/content/browser/bluetooth/advertisement_client.cc index 4672827..7e2bf39d 100644 --- a/content/browser/bluetooth/advertisement_client.cc +++ b/content/browser/bluetooth/advertisement_client.cc @@ -105,6 +105,22 @@ // TODO(crbug.com/40707749): Filter out advertisement data if not // included in the filters, optionalServices, or optionalManufacturerData. auto filtered_event = event.Clone(); + + std::erase_if(filtered_event->uuids, [](const BluetoothUUID& uuid) { + return BluetoothBlocklist::Get().IsExcluded(uuid); + }); + base::EraseIf( + filtered_event->service_data, + [](const std::pair<BluetoothUUID, std::vector<uint8_t>>& entry) { + return BluetoothBlocklist::Get().IsExcluded(entry.first); + }); + base::EraseIf(filtered_event->manufacturer_data, + [](const std::pair<blink::mojom::WebBluetoothCompanyPtr, + std::vector<uint8_t>>& entry) { + return BluetoothBlocklist::Get().IsExcluded(entry.first->id, + entry.second); + }); + if (options_->accept_all_advertisements) { if (prompt_controller_) { AddFilteredDeviceToPrompt(filtered_event->device->id.str(), diff --git a/third_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js b/third_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js new file mode 100644 index 0000000..31af32e --- /dev/null +++ b/third_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js @@ -0,0 +1,82 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/bluetooth/resources/bluetooth-test.js +// META: script=/bluetooth/resources/bluetooth-fake-devices.js +// META: timeout=long +'use strict'; +const test_desc = `Blocked manufacturer data, service UUIDs and service data ` + + `are filtered from the advertisement event in requestLEScan.`; + +const blocklistedServiceUUID = BluetoothUUID.getService(0x1812); +const blocklistedServiceData = new Uint8Array([1, 2, 3]); + +const advertisement_packet_with_blocked_data = { + deviceAddress: '07:07:07:07:07:07', + rssi: -10, + scanRecord: { + name: 'LE Device', + uuids: [uuid1234, blocklistedServiceUUID], + manufacturerData: { + [nonBlocklistedManufacturerId]: nonBlocklistedManufacturerData, + [blocklistedManufacturerId]: blocklistedManufacturerData, + }, + serviceData: { + [uuid1234]: uuid1234Data, + [blocklistedServiceUUID]: blocklistedServiceData, + }, + } +}; + +bluetooth_test(async (t) => { + const fake_central = + await navigator.bluetooth.test.simulateCentral({state: 'powered-on'}); + + const watcher = + new EventWatcher(t, navigator.bluetooth, ['advertisementreceived']); + + await callWithTrustedClick(async () => { + await navigator.bluetooth.requestLEScan({ + filters: [{services: [uuid1234]}], + }); + }); + + let advertisementreceivedPromise = watcher.wait_for('advertisementreceived'); + fake_central.simulateAdvertisementReceived( + advertisement_packet_with_blocked_data); + let evt = await advertisementreceivedPromise; + + // Check if non blocked-listed manufacturer still exists. + assert_true( + evt.manufacturerData.has(nonBlocklistedManufacturerId), + 'Non-blocklisted manufacturer data should be present.'); + assert_data_maps_equal( + evt.manufacturerData, /*expected_key=*/ nonBlocklistedManufacturerId, + nonBlocklistedManufacturerData); + + // Check if block-listed manufacturer data is filtered out properly. + assert_false( + evt.manufacturerData.has(blocklistedManufacturerId), + 'Blocklisted manufacturer data should be filtered out.'); + + // Check if non blocked-listed service UUID still exists. + assert_true( + evt.uuids.includes(uuid1234), + 'Non-blocklisted service UUID should be present.'); + + // Check if block-listed service UUID is filtered out properly. + assert_false( + evt.uuids.includes(blocklistedServiceUUID), + 'Blocklisted service UUID should be filtered out.'); + + // Check if non blocked-listed service data still exists. + assert_true( + evt.serviceData.has(uuid1234), + 'Non-blocklisted service data should be present.'); + assert_data_maps_equal( + evt.serviceData, /*expected_key=*/ uuid1234, uuid1234Data); + + // Check if block-listed service data is filtered out properly. + assert_false( + evt.serviceData.has(blocklistedServiceUUID), + 'Blocklisted service data should be filtered out.'); +}, test_desc);
Regression Test / PoC
diff --git a/third_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js b/third_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js
new file mode 100644
index 0000000..31af32e
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/bluetooth/requestLEScan/blocklisted-manufacturer-data-filtered-from-event.https.window.js
@@ -0,0 +1,82 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// META: timeout=long
+'use strict';
+const test_desc = `Blocked manufacturer data, service UUIDs and service data ` +
+ `are filtered from the advertisement event in requestLEScan.`;
+
+const blocklistedServiceUUID = BluetoothUUID.getService(0x1812);
+const blocklistedServiceData = new Uint8Array([1, 2, 3]);
+
+const advertisement_packet_with_blocked_data = {
+ deviceAddress: '07:07:07:07:07:07',
+ rssi: -10,
+ scanRecord: {
+ name: 'LE Device',
+ uuids: [uuid1234, blocklistedServiceUUID],
+ manufacturerData: {
+ [nonBlocklistedManufacturerId]: nonBlocklistedManufacturerData,
+ [blocklistedManufacturerId]: blocklistedManufacturerData,
+ },
+ serviceData: {
+ [uuid1234]: uuid1234Data,
+ [blocklistedServiceUUID]: blocklistedServiceData,
+ },
+ }
+};
+
+bluetooth_test(async (t) => {
+ const fake_central =
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
+
+ const watcher =
+ new EventWatcher(t, navigator.bluetooth, ['advertisementreceived']);
+
+ await callWithTrustedClick(async () => {
+ await navigator.bluetooth.requestLEScan({
+ filters: [{services: [uuid1234]}],
+ });
+ });
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ fake_central.simulateAdvertisementReceived(
+ advertisement_packet_with_blocked_data);
+ let evt = await advertisementreceivedPromise;
+
+ // Check if non blocked-listed manufacturer still exists.
+ assert_true(
+ evt.manufacturerData.has(nonBlocklistedManufacturerId),
+ 'Non-blocklisted manufacturer data should be present.');
+ assert_data_maps_equal(
+ evt.manufacturerData, /*expected_key=*/ nonBlocklistedManufacturerId,
+ nonBlocklistedManufacturerData);
+
+ // Check if block-listed manufacturer data is filtered out properly.
+ assert_false(
+ evt.manufacturerData.has(blocklistedManufacturerId),
+ 'Blocklisted manufacturer data should be filtered out.');
+
+ // Check if non blocked-listed service UUID still exists.
+ assert_true(
+ evt.uuids.includes(uuid1234),
+ 'Non-blocklisted service UUID should be present.');
+
+ // Check if block-listed service UUID is filtered out properly.
+ assert_false(
+ evt.uuids.includes(blocklistedServiceUUID),
+ 'Blocklisted service UUID should be filtered out.');
+
+ // Check if non blocked-listed service data still exists.
+ assert_true(
+ evt.serviceData.has(uuid1234),
+ 'Non-blocklisted service data should be present.');
+ assert_data_maps_equal(
+ evt.serviceData, /*expected_key=*/ uuid1234, uuid1234Data);
+
+ // Check if block-listed service data is filtered out properly.
+ assert_false(
+ evt.serviceData.has(blocklistedServiceUUID),
+ 'Blocklisted service data should be filtered out.');
+}, test_desc);
Original Bug Report
Privacy bypass: Web Bluetooth Scanning Client leaks blocklisted manufacturer data
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The Web Bluetooth ScanningClient fails to filter advertisement data against the BluetoothBlocklist before sending it to the renderer. This allows a compromised renderer to access restricted manufacturer data, such as iBeacon proximity UUIDs used for location tracking. Furthermore, the browser process lacks a feature flag check, allowing a compromised renderer to trigger this even if the feature is disabled.
Affected files:
content/browser/bluetooth/advertisement_client.cccontent/browser/bluetooth/web_bluetooth_service_impl.cc
Estimated timestamp from git blame: 2026-01-13
Description
A potential privacy bypass exists in the Web Bluetooth scanning implementation. Specifically, WebBluetoothServiceImpl::ScanningClient::SendEvent fails to filter incoming advertisement events against the BluetoothBlocklist. This allows restricted manufacturer data (such as Apple iBeacon 0x004c proximity data, which can reveal a user’s physical location) to be sent to the renderer.
By contrast, the sibling class WatchAdvertisementsClient::SendEvent in the same file correctly strips out this excluded data using BluetoothBlocklist::Get().IsExcluded().
Additionally, the browser-side Mojo endpoint WebBluetoothServiceImpl::RequestScanningStart does not verify if the WebBluetoothScanning feature flag is enabled. This allows a compromised renderer to invoke the scanning interface unconditionally, bypassing the JavaScript-level feature gate.
Potential Attacker Steps
Please note: These are suggested steps identified by our setup and have not yet been verified with a working Proof of Concept.
- Compromise Renderer: An attacker exploits an independent vulnerability to gain arbitrary code execution in a renderer process.
- Invoke Mojo Interface: The attacker directly calls the
blink.mojom.WebBluetoothService.RequestScanningStartMojo method, bypassing Blink’s JS feature gates. Because the browser process lacks a feature flag check forWebBluetoothScanning, the request proceeds. - Social Engineering: The attacker uses a deceptive UI overlay to trick the user into granting the native Bluetooth scanning permission prompt.
- Receive Blocklisted Data: A nearby device (e.g., an Apple iBeacon) broadcasts advertisement data. The browser’s Bluetooth stack receives this and routes it to
ScanningClient::SendEventincontent/browser/bluetooth/advertisement_client.cc. - Extract Location: Because
ScanningClient::SendEventsimply clones the event and skips blocklist filtering, the raw, unfiltered event is sent over Mojo to the renderer. The attacker extracts the blocklisted proximity UUIDs (under company ID0x004c) to track the user’s physical location.
Suggested Fix
- Apply Blocklist Filtering: In
content/browser/bluetooth/advertisement_client.cc, updateWebBluetoothServiceImpl::ScanningClient::SendEventto filteruuids,service_data, andmanufacturer_datausingBluetoothBlocklist::Get().IsExcluded(). This should mirror the filtering logic already present inWatchAdvertisementsClient::SendEvent. - Enforce Feature Flag: In
content/browser/bluetooth/web_bluetooth_service_impl.cc, add abase::FeatureList::IsEnabledcheck for the correspondingWebBluetoothScanningfeature flag at the beginning ofWebBluetoothServiceImpl::RequestScanningStart. If the feature is disabled, the Mojo request should be rejected (e.g., viamojo::ReportBadMessage).
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.