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 <[email protected]> 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 <[email protected]> Reviewed-by: Jack Shira <[email protected]> Commit-Queue: Alvin Ji <[email protected]> Reviewed-by: Avi Drissman <[email protected]> Reviewed-by: Matt Reynolds <[email protected]> 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