Chrome · Bluetooth
CVE-2026-17906
Logic Error in Bluetooth
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/bluetooth/web_bluetooth_service_impl.cc |
modified |
Files Changed
chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cccontent/browser/bluetooth/bluetooth_metrics.hcontent/browser/bluetooth/web_bluetooth_service_impl.cc
Patch
From b2b3050f4ddb21ca102af7e9e819a6b735505411 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Wed, 03 Jun 2026 17:34:38 -0700
Subject: [PATCH] bluetooth: Enforce permission checks on GATT operations
Introduce a new Mojo error code BLUETOOTH_NOT_ALLOWED that maps to a
spec-compliant NetworkError DOMException.
Enforce early GetBluetoothAllowed() permission checks on
WebBluetoothService GATT IPC entry points to prevent unauthenticated
access and return BLUETOOTH_NOT_ALLOWED when permission is denied.
Bug: 497654761
Change-Id: I50cc6887a3dcc36b7397f0d36027fe1b23dfcf22
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7891430
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Reviewed-by: Jack Shira <jackshira@google.com>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1641338}
---
diff --git a/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc b/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
index 0f93651..bc82e81 100644
--- a/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
@@ -212,6 +212,7 @@
}
void TearDownOnMainThread() override {
+ content::SetBluetoothAdapter(nullptr);
content::SetBrowserClientForTesting(old_browser_client_);
ControlledFrameDisabledPermissionTest::TearDownOnMainThread();
}
diff --git a/content/browser/bluetooth/bluetooth_metrics.h b/content/browser/bluetooth/bluetooth_metrics.h
index f0d2a28..1ea6ad268 100644
--- a/content/browser/bluetooth/bluetooth_metrics.h
+++ b/content/browser/bluetooth/bluetooth_metrics.h
@@ -60,10 +60,11 @@
kWakelock = 20,
kUnexpectedState = 21,
kSocketError = 22,
+ kNotAllowed = 23,
// Note: Add new ConnectGATT outcomes immediately above this line. Make sure
// to update the enum list in
// tools/metrics/histograms/metadata/bluetooth/enums.xml accordingly.
- kMaxValue = kSocketError,
+ kMaxValue = kNotAllowed,
};
// There should be a call to this function before every
@@ -157,10 +158,11 @@
kNotPaired = 11,
kNotSupported = 12,
kBlocklisted = 13,
+ kNotAllowed = 14,
// Note: Add new GATT Outcomes immediately above this line.
// Make sure to update the enum list in
// tools/metrics/histograms/histograms.xml accordingly.
- kMaxValue = kBlocklisted
+ kMaxValue = kNotAllowed
};
// Values below do NOT map to UMA metric values.
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc
index 5d78e23..4d6a09f2 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc
@@ -788,6 +788,21 @@
void WebBluetoothServiceImpl::RequestDevice(
blink::mojom::WebBluetoothRequestDeviceOptionsPtr options,
RequestDeviceCallback callback) {
+ blink::mojom::WebBluetoothResult allowed_result = GetBluetoothAllowed();
+ if (allowed_result != blink::mojom::WebBluetoothResult::SUCCESS) {
+ if (allowed_result == blink::mojom::WebBluetoothResult::
+ CHOOSER_NOT_SHOWN_API_GLOBALLY_DISABLED) {
+ // Log to the developer console to maintain the same behavior when the
+ // permission check fails at the chooser side (which we now bypass via
+ // this early return).
+ render_frame_host().AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kInfo,
+ "Bluetooth permission has been blocked.");
+ }
+ std::move(callback).Run(allowed_result, /*device=*/nullptr);
+ return;
+ }
+
if (base::FeatureList::IsEnabled(
features::kWebBluetoothAllowGetAvailabilityWithBfcache)) {
PreventBackForwardCache();
@@ -805,7 +820,7 @@
}
std::move(callback).Run(
blink::mojom::WebBluetoothResult::BLUETOOTH_LOW_ENERGY_NOT_AVAILABLE,
- nullptr /* device */);
+ /*device=*/nullptr);
return;
}
RequestDeviceImpl(std::move(options), std::move(callback), GetAdapter());
@@ -868,6 +883,14 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ // Return BLUETOOTH_NOT_ALLOWED (which maps to a spec-compliant
+ // NetworkError) when permission is denied.
+ std::move(callback).Run(
+ blink::mojom::WebBluetoothResult::BLUETOOTH_NOT_ALLOWED);
+ return;
+ }
+
bool is_connect_allowed = false;
if (base::FeatureList::IsEnabled(
features::kWebBluetoothNewPermissionsBackend)) {
@@ -927,6 +950,10 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ return;
+ }
+
if (base::FeatureList::IsEnabled(
blink::features::kWebBluetoothCancelConnect)) {
auto connecting_iter = pending_connection_device_ids_.find(device_id);
@@ -955,6 +982,15 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ // Return BLUETOOTH_NOT_ALLOWED (which maps to a spec-compliant
+ // NetworkError) when permission is denied.
+ std::move(callback).Run(
+ blink::mojom::WebBluetoothResult::BLUETOOTH_NOT_ALLOWED,
+ /*services=*/std::nullopt);
+ return;
+ }
+
RecordGetPrimaryServicesServices(quantity, services_uuid);
if (!IsAllowedToAccessAtLeastOneService(device_id)) {
@@ -996,6 +1032,15 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ // Return BLUETOOTH_NOT_ALLOWED (which maps to a spec-compliant
+ // NetworkError) when permission is denied.
+ std::move(callback).Run(
+ blink::mojom::WebBluetoothResult::BLUETOOTH_NOT_ALLOWED,
+ /*characteristics=*/std::nullopt);
+ return;
+ }
+
RecordGetCharacteristicsCharacteristic(quantity, characteristics_uuid);
if (characteristics_uuid &&
@@ -1071,6 +1116,15 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ // Return BLUETOOTH_NOT_ALLOWED (which maps to a spec-compliant
+ // NetworkError) when permission is denied.
+ std::move(callback).Run(
+ blink::mojom::WebBluetoothResult::BLUETOOTH_NOT_ALLOWED,
+ /*descriptors=*/std::nullopt);
+ return;
+ }
+
if (descriptors_uuid &&
BluetoothBlocklist::Get().IsExcluded(descriptors_uuid.value())) {
std::move(callback).Run(
@@ -1137,6 +1191,15 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ // Return BLUETOOTH_NOT_ALLOWED (which maps to a spec-compliant
+ // NetworkError) when permission is denied.
+ std::move(callback).Run(
+ blink::mojom::WebBluetoothResult::BLUETOOTH_NOT_ALLOWED,
+ /*value=*/{});
+ return;
+ }
+
const CacheQueryResult query_result =
QueryCacheForCharacteristic(characteristic_instance_id);
@@ -1173,6 +1236,14 @@
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(back_forward_cache_feature_handle_.IsValid());
+ if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+ // Return BLUETOOTH_NOT_ALLOWED (which maps to a spec-compliant
+ // NetworkError) when permission is denied.
+ std::move(callback).Run(
+ blink::mojom::WebBluetoothResult::BLUETOOTH_NOT_ALLOWED);
+ return;
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc b/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
index 0f93651..bc82e81 100644
--- a/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
@@ -212,6 +212,7 @@
}
void TearDownOnMainThread() override {
+ content::SetBluetoothAdapter(nullptr);
content::SetBrowserClientForTesting(old_browser_client_);
ControlledFrameDisabledPermissionTest::TearDownOnMainThread();
}
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_browsertest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_browsertest.cc
index 1148630..6bed1682 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_browsertest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_browsertest.cc
@@ -268,6 +268,11 @@
SetFakeBlueboothAdapter();
}
+ void TearDownOnMainThread() override {
+ BluetoothAdapterFactoryWrapper::Get().SetBluetoothAdapterOverride(nullptr);
+ ContentBrowserTest::TearDownOnMainThread();
+ }
+
void SetUpCommandLine(base::CommandLine* command_line) override {
// Sets up the blink runtime feature for accessing to navigator.bluetooth.
command_line->AppendSwitch(
@@ -573,8 +578,6 @@
GURL url = embedded_test_server()->GetURL("/page_with_blank_iframe.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
- EXPECT_CALL(*adapter(), AddObserver(_));
-
RenderFrameHost* sub_frame = ChildFrameAt(GetWebContents(), 0);
ASSERT_TRUE(sub_frame);
@@ -598,7 +601,6 @@
console_observer.messages();
EXPECT_EQ(messages.size(), 1u);
EXPECT_EQ(messages.back().source_frame, sub_frame);
- EXPECT_CALL(*adapter(), RemoveObserver(_));
}
class WebBluetoothServiceImplFencedFramesBrowserTest
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