CVE-2026-13903
Overview
Background
- Web Bluetooth GATT blocklist
BluetoothBlocklist, a security list that marks specific GATT UUIDs as excluded from reads and/or writes so that websites cannot access dangerous attributes; enforced in the browser process.- GATT descriptor vs. characteristic
- a descriptor is a child attribute of a characteristic providing metadata; the blocklist can exclude a characteristic while a descriptor under it is not itself listed.
- `IsExcludedFromReads` / `IsExcludedFromWrites`
BluetoothBlocklistpredicates that return whether a given UUID is barred from read or write access.- `RemoteDescriptorReadValue` / `RemoteDescriptorWriteValue`
- the
WebBluetoothServiceImplMojo handlers that service a renderer’s request to read or write a descriptor value.
Root Cause Analysis
The descriptor read and write handlers in web_bluetooth_service_impl.cc checked only the descriptor’s own UUID against the blocklist (IsExcludedFromReads/IsExcludedFromWrites on query_result.descriptor->GetUUID()), never the UUID of the descriptor’s parent characteristic. The security invariant — that a characteristic which is blocklisted must not be accessible through any of its attributes — was violated, because a website could reach a protected characteristic’s data through one of its (non-blocklisted) descriptors.
The fix ORs in an additional check on query_result.characteristic->GetUUID() in both handlers, so a read or write is rejected with BLOCKLISTED_READ/BLOCKLISTED_WRITE if either the descriptor or its parent characteristic is blocklisted. The regression tests confirm that after blocklisting the parent characteristic’s UUID, RemoteDescriptorReadValue and RemoteDescriptorWriteValue return the blocklisted results.
Attack Path
- Connect to a device with a blocklisted characteristic A malicious page uses Web Bluetooth to connect and gains a GATT service containing a characteristic whose UUID is on the blocklist (excluded from reads and/or writes).
- Enumerate the descriptor
The page calls
RemoteCharacteristicGetDescriptorsto obtain a descriptor instance id under that blocklisted characteristic (the descriptor itself not being blocklisted). - Bypass via descriptor read/write
The page calls
RemoteDescriptorReadValueorRemoteDescriptorWriteValueon that descriptor; pre-patch, only the descriptor UUID was checked, so the operation proceeded despite the parent characteristic being protected. - Access protected data The read returns descriptor data of, or the write mutates, a characteristic the blocklist was meant to shield, defeating the policy control.
Impact Assessment
Files Changed
content/browser/bluetooth/web_bluetooth_service_impl.cccontent/browser/bluetooth/web_bluetooth_service_impl.hcontent/browser/bluetooth/web_bluetooth_service_impl_unittest.ccthird_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
Audit Directions
- Parent/child attribute enforcementReview every access-control check on hierarchical objects (services → characteristics → descriptors) to confirm that restrictions on a parent are inherited by children rather than checked only at the leaf.
- Blocklist coverage per entry pointAudit all Web Bluetooth Mojo handlers (
RemoteCharacteristic*,RemoteDescriptor*, service-level reads/writes) to ensure each independently enforces the blocklist for the full ancestry of the target UUID. - Indirect access pathsLook for cases where a protected resource can be reached through a related, unlisted object (aliasing, metadata attributes, notify sessions) that skips the primary policy gate.
Patch
From a0b55d797bbc3d7dfd31113f4957df873d50d20b Mon Sep 17 00:00:00 2001 From: Rob Pitkin <[email protected]> Date: Fri, 15 May 2026 16:40:45 -0700 Subject: [PATCH] bluetooth: Fix missing blocklist check for characteristic in descriptors Web Bluetooth fails to check the GATT blocklist for a descriptor's parent characteristic during read and write operations. This allows websites to bypass security restrictions and interact with descriptors of protected characteristics. This CL adds checks in `RemoteDescriptorReadValue` and `RemoteDescriptorWriteValue` to ensure that the parent characteristic is also checked against the GATT blocklist before allowing read or write operations on its descriptors. Unit tests are added to verify that reads and writes are blocked when the parent characteristic is blocklisted. Bug: 503912196 Change-Id: If62e10d5ac66ed414aec1a3a4af346133a584374 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7837659 Commit-Queue: Rob Pitkin <[email protected]> Reviewed-by: Matt Reynolds <[email protected]> Cr-Commit-Position: refs/heads/main@{#1631633} --- diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc index a075173..deea337 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl.cc +++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc @@ -1383,7 +1383,9 @@ } if (BluetoothBlocklist::Get().IsExcludedFromReads( - query_result.descriptor->GetUUID())) { + query_result.descriptor->GetUUID()) || + BluetoothBlocklist::Get().IsExcludedFromReads( + query_result.characteristic->GetUUID())) { std::move(callback).Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ, /*value=*/{}); return; @@ -1423,7 +1425,9 @@ } if (BluetoothBlocklist::Get().IsExcludedFromWrites( - query_result.descriptor->GetUUID())) { + query_result.descriptor->GetUUID()) || + BluetoothBlocklist::Get().IsExcludedFromWrites( + query_result.characteristic->GetUUID())) { std::move(callback).Run( blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE); return; diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.h b/content/browser/bluetooth/web_bluetooth_service_impl.h index c414992..73196dd4 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl.h +++ b/content/browser/bluetooth/web_bluetooth_service_impl.h @@ -178,6 +178,12 @@ EmulatedAdapterRemovalRestoresOriginalAdapter); FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, ServiceDestroyedDuringAdapterAcquisition); + FRIEND_TEST_ALL_PREFIXES( + WebBluetoothServiceImplTest, + RemoteDescriptorReadValue_ParentCharacteristicBlocklisted); + FRIEND_TEST_ALL_PREFIXES( + WebBluetoothServiceImplTest, + RemoteDescriptorWriteValue_ParentCharacteristicBlocklisted); #if PAIR_BLUETOOTH_ON_DEMAND() FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc index 42523def..4880d79 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc +++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc @@ -32,6 +32,7 @@ #include "device/bluetooth/test/mock_bluetooth_adapter.h" #include "device/bluetooth/test/mock_bluetooth_device.h" #include "device/bluetooth/test/mock_bluetooth_gatt_characteristic.h" +#include "device/bluetooth/test/mock_bluetooth_gatt_descriptor.h" #include "device/bluetooth/test/mock_bluetooth_gatt_notify_session.h" #include "device/bluetooth/test/mock_bluetooth_gatt_service.h" #include "mojo/public/cpp/bindings/associated_receiver.h" @@ -96,6 +97,8 @@ const device::BluetoothUUID kBatteryServiceUUID(kBatteryServiceUUIDString); const device::BluetoothUUID kBatteryLevelCharacteristicUUID( "00002a19-0000-1000-8000-00805f9b34fb"); +const device::BluetoothUUID kCharacteristicUserDescriptionDescriptorUUID( + "00002901-0000-1000-8000-00805f9b34fb"); const BluetoothDeviceBundleData battery_device_bundle_data = { kBatteryServiceId, kBatteryLevelCharacteristicId, kBatteryServiceUUID, kBatteryLevelCharacteristicUUID, kTestCharacteristicProperties}; @@ -559,6 +562,7 @@ heart_rate_device_bundle_.reset(); service_ptr_ = nullptr; SetBrowserClientForTesting(old_browser_client_); + BluetoothBlocklist::Get().ResetToDefaultValuesForTest(); RenderViewHostImplTestHarness::TearDown(); } @@ -1442,4 +1446,95 @@ EXPECT_CALL(*adapter_, StopScan).Times(1); } +TEST_F(WebBluetoothServiceImplTest, + RemoteDescriptorReadValue_ParentCharacteristicBlocklisted) { + RegisterTestCharacteristic(); + + FakeBluetoothCharacteristic& test_characteristic = + battery_device_bundle().characteristic(); + + auto mock_descriptor = + std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>( + &test_characteristic, "test_descriptor_id", + kCharacteristicUserDescriptionDescriptorUUID, + device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE); + + test_characteristic.AddMockDescriptor(std::move(mock_descriptor)); + + base::test::TestFuture< + WebBluetoothResult, + std::optional< + std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>> + get_descriptors_future; + service_ptr_->RemoteCharacteristicGetDescriptors( + test_characteristic.GetIdentifier(), + WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt, + get_descriptors_future.GetCallback()); + + EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS); + + const auto& descriptors = get_descriptors_future.Get<1>(); + ASSERT_TRUE(descriptors.has_value()); + ASSERT_FALSE(descriptors->empty()); + std::string descriptor_instance_id = descriptors->at(0)->instance_id; + + BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(), + BluetoothBlocklist::Value::EXCLUDE_READS); + + base::test::TestFuture<WebBluetoothResult, std::vector<uint8_t>> read_future; + service_ptr_->RemoteDescriptorReadValue( + descriptor_instance_id, + base::BindLambdaForTesting( + [&read_future](WebBluetoothResult result, + base::span<const uint8_t> value) { + read_future.SetValue( + result, std::vector<uint8_t>(value.begin(), value.end())); + })); + + EXPECT_EQ(read_future.Get<0>(), WebBluetoothResult::BLOCKLISTED_READ); +} + +TEST_F(WebBluetoothServiceImplTest, + RemoteDescriptorWriteValue_ParentCharacteristicBlocklisted) { + RegisterTestCharacteristic(); + + FakeBluetoothCharacteristic& test_characteristic = + battery_device_bundle().characteristic(); + + auto mock_descriptor = + std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>( + &test_characteristic, "test_descriptor_id", + kCharacteristicUserDescriptionDescriptorUUID, + device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE); + + test_characteristic.AddMockDescriptor(std::move(mock_descriptor)); + + base::test::TestFuture< + WebBluetoothResult, + std::optional< + std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>> + get_descriptors_future; + service_ptr_->RemoteCharacteristicGetDescriptors( + test_characteristic.GetIdentifier(), + WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt, + get_descriptors_future.GetCallback()); + + EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS); + + const auto& descriptors = get_descriptors_future.Get<1>(); + ASSERT_TRUE(descriptors.has_value()); + ASSERT_FALSE(descriptors->empty()); + std::string descriptor_instance_id = descriptors->at(0)->instance_id; + + BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(), + BluetoothBlocklist::Value::EXCLUDE_WRITES); + + std::vector<uint8_t> value = {1, 2, 3}; + base::test::TestFuture<WebBluetoothResult> write_future; + service_ptr_->RemoteDescriptorWriteValue(descriptor_instance_id, value, + write_future.GetCallback()); + + EXPECT_EQ(write_future.Get(), WebBluetoothResult::BLOCKLISTED_WRITE); +} + } // namespace content diff --git a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js index c503bf7..37e126df 100644 --- a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js +++ b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js @@ -29,6 +29,7 @@ 'bad1c9a2-9a5b-4015-8b60-1579bbbf2135';
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 42523def..4880d79 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -32,6 +32,7 @@
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_characteristic.h"
+#include "device/bluetooth/test/mock_bluetooth_gatt_descriptor.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_notify_session.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_service.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
@@ -96,6 +97,8 @@
const device::BluetoothUUID kBatteryServiceUUID(kBatteryServiceUUIDString);
const device::BluetoothUUID kBatteryLevelCharacteristicUUID(
"00002a19-0000-1000-8000-00805f9b34fb");
+const device::BluetoothUUID kCharacteristicUserDescriptionDescriptorUUID(
+ "00002901-0000-1000-8000-00805f9b34fb");
const BluetoothDeviceBundleData battery_device_bundle_data = {
kBatteryServiceId, kBatteryLevelCharacteristicId, kBatteryServiceUUID,
kBatteryLevelCharacteristicUUID, kTestCharacteristicProperties};
@@ -559,6 +562,7 @@
heart_rate_device_bundle_.reset();
service_ptr_ = nullptr;
SetBrowserClientForTesting(old_browser_client_);
+ BluetoothBlocklist::Get().ResetToDefaultValuesForTest();
RenderViewHostImplTestHarness::TearDown();
}
@@ -1442,4 +1446,95 @@
EXPECT_CALL(*adapter_, StopScan).Times(1);
}
+TEST_F(WebBluetoothServiceImplTest,
+ RemoteDescriptorReadValue_ParentCharacteristicBlocklisted) {
+ RegisterTestCharacteristic();
+
+ FakeBluetoothCharacteristic& test_characteristic =
+ battery_device_bundle().characteristic();
+
+ auto mock_descriptor =
+ std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>(
+ &test_characteristic, "test_descriptor_id",
+ kCharacteristicUserDescriptionDescriptorUUID,
+ device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE);
+
+ test_characteristic.AddMockDescriptor(std::move(mock_descriptor));
+
+ base::test::TestFuture<
+ WebBluetoothResult,
+ std::optional<
+ std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>>
+ get_descriptors_future;
+ service_ptr_->RemoteCharacteristicGetDescriptors(
+ test_characteristic.GetIdentifier(),
+ WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt,
+ get_descriptors_future.GetCallback());
+
+ EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS);
+
+ const auto& descriptors = get_descriptors_future.Get<1>();
+ ASSERT_TRUE(descriptors.has_value());
+ ASSERT_FALSE(descriptors->empty());
+ std::string descriptor_instance_id = descriptors->at(0)->instance_id;
+
+ BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(),
+ BluetoothBlocklist::Value::EXCLUDE_READS);
+
+ base::test::TestFuture<WebBluetoothResult, std::vector<uint8_t>> read_future;
+ service_ptr_->RemoteDescriptorReadValue(
+ descriptor_instance_id,
+ base::BindLambdaForTesting(
+ [&read_future](WebBluetoothResult result,
+ base::span<const uint8_t> value) {
+ read_future.SetValue(
+ result, std::vector<uint8_t>(value.begin(), value.end()));
+ }));
+
+ EXPECT_EQ(read_future.Get<0>(), WebBluetoothResult::BLOCKLISTED_READ);
+}
+
+TEST_F(WebBluetoothServiceImplTest,
+ RemoteDescriptorWriteValue_ParentCharacteristicBlocklisted) {
+ RegisterTestCharacteristic();
+
+ FakeBluetoothCharacteristic& test_characteristic =
+ battery_device_bundle().characteristic();
+
+ auto mock_descriptor =
+ std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>(
+ &test_characteristic, "test_descriptor_id",
+ kCharacteristicUserDescriptionDescriptorUUID,
+ device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE);
+
+ test_characteristic.AddMockDescriptor(std::move(mock_descriptor));
+
+ base::test::TestFuture<
+ WebBluetoothResult,
+ std::optional<
+ std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>>
+ get_descriptors_future;
+ service_ptr_->RemoteCharacteristicGetDescriptors(
+ test_characteristic.GetIdentifier(),
+ WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt,
+ get_descriptors_future.GetCallback());
+
+ EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS);
+
+ const auto& descriptors = get_descriptors_future.Get<1>();
+ ASSERT_TRUE(descriptors.has_value());
+ ASSERT_FALSE(descriptors->empty());
+ std::string descriptor_instance_id = descriptors->at(0)->instance_id;
+
+ BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(),
+ BluetoothBlocklist::Value::EXCLUDE_WRITES);
+
+ std::vector<uint8_t> value = {1, 2, 3};
+ base::test::TestFuture<WebBluetoothResult> write_future;
+ service_ptr_->RemoteDescriptorWriteValue(descriptor_instance_id, value,
+ write_future.GetCallback());
+
+ EXPECT_EQ(write_future.Get(), WebBluetoothResult::BLOCKLISTED_WRITE);
+}
+
} // namespace content
diff --git a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
index c503bf7..37e126df 100644
--- a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
+++ b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
@@ -29,6 +29,7 @@
'bad1c9a2-9a5b-4015-8b60-1579bbbf2135';
var request_disconnection_characteristic_uuid =
'01d7d88a-7451-419f-aeb8-d65e7b9277af';
+var heart_rate_measurement_uuid = '00002a37-0000-1000-8000-00805f9b34fb';
/* Descriptor UUIDs */
var blocklist_test_descriptor_uuid = 'bad2ddcf-60db-45cd-bef9-fd72b153cf7c';
@@ -610,12 +611,25 @@
* objects.
*/
async function getBlocklistExcludeReadsDescriptor() {
- let result = await getBlocklistExcludeWritesCharacteristic();
- let descriptor = await result.characteristic.getDescriptor(
- blocklist_exclude_reads_descriptor_uuid);
+ let result = await getBlocklistTestService();
+
+ let fake_heart_rate_characteristic =
+ await result.fake_service.addFakeCharacteristic({
+ uuid: heart_rate_measurement_uuid,
+ properties: ['read', 'write'],
+ });
+
+ let fake_descriptor = await fake_heart_rate_characteristic.addFakeDescriptor(
+ {uuid: blocklist_exclude_reads_descriptor_uuid});
+
+ let characteristic = await result.service.getCharacteristic(heart_rate_measurement_uuid);
+ let descriptor = await characteristic.getDescriptor(blocklist_exclude_reads_descriptor_uuid);
+
return Object.assign(result, {
+ characteristic,
+ fake_characteristic: fake_heart_rate_characteristic,
descriptor,
- fake_descriptor: result.fake_blocklist_exclude_reads_descriptor
+ fake_descriptor
});
}
@@ -641,12 +655,25 @@
* objects.
*/
async function getBlocklistExcludeWritesDescriptor() {
- let result = await getBlocklistExcludeWritesCharacteristic();
- let descriptor = await result.characteristic.getDescriptor(
- 'gatt.client_characteristic_configuration');
+ let result = await getBlocklistTestService();
+
+ let fake_heart_rate_characteristic =
+ await result.fake_service.addFakeCharacteristic({
+ uuid: heart_rate_measurement_uuid,
+ properties: ['read', 'write'],
+ });
+
+ let fake_descriptor = await fake_heart_rate_characteristic.addFakeDescriptor(
+ {uuid: 'gatt.client_characteristic_configuration'});
+
+ let characteristic = await result.service.getCharacteristic(heart_rate_measurement_uuid);
+ let descriptor = await characteristic.getDescriptor('gatt.client_characteristic_configuration');
+
return Object.assign(result, {
- descriptor: descriptor,
- fake_descriptor: result.fake_blocklist_exclude_writes_descriptor,
+ characteristic,
+ fake_characteristic: fake_heart_rate_characteristic,
+ descriptor,
+ fake_descriptor
});
}
Original Bug Report
Missing GATT blocklist check for parent characteristic in Web Bluetooth descriptors
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Web Bluetooth implementation fails to check the GATT blocklist for a descriptor’s parent characteristic during read and write operations. This allows websites to bypass security restrictions and interact with descriptors of protected characteristics, violating the Web Bluetooth specification.
Affected files:
content/browser/bluetooth/web_bluetooth_service_impl.cc
Estimated timestamp from git blame: 2024-10-23
Summary
The GATT blocklist in Web Bluetooth is designed to restrict access to sensitive services, characteristics, and descriptors. However, the implementation of GATT descriptor read and write operations in WebBluetoothServiceImpl fails to verify the blocklist status of the descriptor’s parent characteristic. This omission allows a malicious site to read or write descriptors associated with a partially blocked characteristic, bypassing the security and privacy protections intended by the GATT blocklist.
Technical Details
In content/browser/bluetooth/web_bluetooth_service_impl.cc, the methods RemoteDescriptorReadValue and RemoteDescriptorWriteValue handle GATT descriptor interactions.
According to the Web Bluetooth specification:
- For
readValue()on a descriptor (§ 13.5): “If characteristic is in the Blocked Characteristic for Read UUIDs list, then return a promise rejected with a ‘SecurityError’ DOMException.” - For
writeValue()on a descriptor (§ 13.5): “If characteristic is in the Blocked Characteristic for Write UUIDs list, then return a promise rejected with a ‘SecurityError’ DOMException.”
The current implementation only checks whether the descriptor’s own UUID is in the blocklist, ignoring the status of the characteristic that owns it.
Vulnerable Code Snippets
In WebBluetoothServiceImpl::RemoteDescriptorReadValue:
1378: if (BluetoothBlocklist::Get().IsExcludedFromReads(
1379: query_result.descriptor->GetUUID())) {
1380: std::move(callback).Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ,
1381: /*value=*/{});
1382: return;
1383: }
In WebBluetoothServiceImpl::RemoteDescriptorWriteValue:
1418: if (BluetoothBlocklist::Get().IsExcludedFromWrites(
1419: query_result.descriptor->GetUUID())) {
1420: std::move(callback).Run(
1421: blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE);
1422: return;
1423: }
Because characteristics entirely excluded from the blocklist (marked with Value::EXCLUDE) are filtered during discovery (WebBluetoothServiceImpl::RemoteServiceGetCharacteristics), they are unaffected. However, partially restricted characteristics (e.g., those marked with Value::EXCLUDE_READS or Value::EXCLUDE_WRITES) remain discoverable by the renderer, allowing an attacker to request their descriptors.
Potential Attack Steps
These are potential steps an attacker would follow to trigger the vulnerability:
- An attacker hosts a malicious website and tricks a user into pairing a Bluetooth device that exposes a partially blocklisted characteristic (e.g.,
2a02Peripheral Privacy Flag, which isEXCLUDE_WRITES). - The attacker’s JavaScript connects to the GATT server and requests the partially blocklisted characteristic (e.g.,
service.getCharacteristic('2a02')). This succeeds because it is not fully excluded. - The script retrieves the descriptors associated with this characteristic by calling
characteristic.getDescriptors(). - The script attempts to read or write to one of these descriptors (e.g.,
descriptor.writeValue(new Uint8Array([...]))). - The browser verifies the descriptor’s UUID against the blocklist, but fails to check the parent characteristic’s UUID.
- The browser allows the write operation, successfully bypassing the GATT blocklist restriction.
Suggested Fix
In WebBluetoothServiceImpl::RemoteDescriptorReadValue and WebBluetoothServiceImpl::RemoteDescriptorWriteValue, add a check for the parent characteristic’s blocklist status before allowing the operation.
For RemoteDescriptorReadValue:
if (BluetoothBlocklist::Get().IsExcludedFromReads(
query_result.descriptor->GetUUID()) ||
BluetoothBlocklist::Get().IsExcludedFromReads(
query_result.characteristic->GetUUID())) {
std::move(callback).Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ,
/*value=*/{});
return;
}
For RemoteDescriptorWriteValue:
if (BluetoothBlocklist::Get().IsExcludedFromWrites(
query_result.descriptor->GetUUID()) ||
BluetoothBlocklist::Get().IsExcludedFromWrites(
query_result.characteristic->GetUUID())) {
std::move(callback).Run(
blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE);
return;
}
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.