Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Bluetooth
DescriptionInsufficient validation of untrusted input in Bluetooth
ComponentBluetooth
Bug ClassLogic Error
Tracker497654761
Fix commitb2b3050f4ddb (chromium/src) +118/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
content/browser/bluetooth/web_bluetooth_service_impl.cc
modified

Files Changed

  • chrome/browser/controlled_frame/controlled_frame_disabled_permission_browsertest.cc
  • content/browser/bluetooth/bluetooth_metrics.h
  • content/browser/bluetooth/web_bluetooth_service_impl.cc
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.