Low watch CVSS 6.5 chrome Policy Bypass 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Bluetooth
DescriptionInsufficient policy enforcement in Bluetooth
ComponentBluetooth
Sub-componentBluetooth
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker519981896
Fix commit1c56c2b5dc9a (chromium/src) +51/-0
CWECWE-284
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-04 (per vendor advisory)
Fix landed2026-06-17 (public git)
Disclosed2026-07-29
Patch gap42 days — fix public in source before the advisory

Background

`WebBluetoothServiceImpl::ForgetDevice`
a browser-process Mojo entry point, callable directly by the renderer, that removes a device from the origin’s set of allowed/permitted Bluetooth devices.
`GetBluetoothAllowed()`
a helper returning blink::mojom::WebBluetoothResult that consolidates the Permissions-Policy check, the BluetoothDelegate::MayUseBluetooth() check, and AllowWebBluetooth() policy, returning SUCCESS only when Bluetooth use is permitted.
`allowed_devices()` / `GetDeviceAddress`
the per-origin store of granted device permissions; a non-empty address means the device is still permitted for that origin.
Permissions-Policy / `MayUseBluetooth`
enterprise/policy and feature-policy gates that can globally disallow Bluetooth for a frame or context.

Root Cause Analysis

Every other GATT/permission-sensitive entry point on WebBluetoothServiceImpl consults GetBluetoothAllowed() before acting, but ForgetDevice was missing that check, so it would mutate the origin’s allowed-devices state even when Bluetooth was disallowed by Permissions-Policy, MayUseBluetooth(), or AllowWebBluetooth(). Because ForgetDevice is a Mojo method the renderer invokes directly, a compromised renderer could call it to revoke previously granted device permissions in a context where it should have no Bluetooth capability at all, violating the invariant that all Bluetooth operations are gated by GetBluetoothAllowed().

The fix inserts an early if (GetBluetoothAllowed() != SUCCESS) { std::move(callback).Run(); return; } at the top of ForgetDevice, before the CHECK and the permissions-backend logic, so a disallowed caller returns immediately without touching device state.

The added regression test ForgetDevice_NotAllowed sets may_use_bluetooth_ to false and confirms the device address is still present after the call, proving the early return preserves state.

Key insight
ForgetDevice was the one Bluetooth IPC entry point missing the GetBluetoothAllowed() gate; the fix adds that check so a renderer cannot mutate allowed-device permissions while Bluetooth is policy-disabled.

Attack Path

  1. Bluetooth disallowed context A frame or origin is in a state where Bluetooth is blocked by Permissions-Policy or MayUseBluetooth()/AllowWebBluetooth() policy, yet still holds a Mojo handle to WebBluetoothServiceImpl.
  2. Direct IPC call A compromised or malicious renderer invokes ForgetDevice(device_id, ...) directly over Mojo, bypassing any UI or higher-level gating.
  3. Unauthorized state mutation Pre-patch, ForgetDevice proceeds without checking GetBluetoothAllowed() and removes the device from the origin’s allowed-devices store despite the policy prohibition.
  4. Post-fix outcome With the patch, GetBluetoothAllowed() returns non-SUCCESS, the callback runs with no effect, and the device permission remains intact.

Impact Assessment

A compromised renderer in a Bluetooth-disallowed context could bypass Permissions-Policy / MayUseBluetooth enforcement to revoke an origin’s granted Bluetooth device permissions, an insufficient-policy-enforcement issue in the browser process. The advisory describes it as insufficient policy enforcement in Bluetooth and states no exploited-in-the-wild status; the concrete effect shown by the patch is unauthorized removal of allowed-device state, with any broader consequence (inferred).

Changed Functions

FunctionChangeNotes
if
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified
TEST_F
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified

Files Changed

  • content/browser/bluetooth/web_bluetooth_service_impl.cc
  • content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc

Audit Directions

  • Entry-point check parity
    Enumerate all Mojo methods on a capability service and confirm each performs the same permission/policy gate; a method missing the shared check (like ForgetDevice) is the classic gap.
  • State-mutating operations
    Pay special attention to IPC methods that revoke, delete, or modify permission/device state (not just those that read or connect), since these are easy to overlook when auditing “access” checks.
  • Delegate-backed policy
    Audit callers of BluetoothDelegate/AllowWebBluetooth-style delegates to ensure every renderer-reachable operation routes through the consolidated allow-check rather than assuming an upstream gate.
From 1c56c2b5dc9a01bdd62896a2bfd5e3394a7d1acc Mon Sep 17 00:00:00 2001
From: Alvin Ji <[email protected]>
Date: Tue, 16 Jun 2026 17:33:59 -0700
Subject: [PATCH] bluetooth: Verify bluetooth is allowed in ForgetDevice

This CL adds the missing GetBluetoothAllowed() check to
WebBluetoothServiceImpl::ForgetDevice. This prevents compromised
renderers from bypassing Permissions-Policy or MayUseBluetooth checks by
calling this entry point.

Change-Id: I02c89437b7e5f7ef9fa7c3cf2a6a72bbeb6906a1
Bug: 519981896
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904063
Reviewed-by: Matt Reynolds <[email protected]>
Commit-Queue: Alvin Ji <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1648006}
---

diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc
index ccc16bb8..87d0e0f 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc
@@ -855,6 +855,10 @@
 void WebBluetoothServiceImpl::ForgetDevice(
     const blink::WebBluetoothDeviceId& device_id,
     ForgetDeviceCallback callback) {
+  if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS) {
+    std::move(callback).Run();
+    return;
+  }
   CHECK(back_forward_cache_feature_handle_.IsValid());
   if (!base::FeatureList::IsEnabled(
           features::kWebBluetoothNewPermissionsBackend)) {
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
index 4880d79..be04cdd 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -372,6 +372,23 @@
     has_device_permission_ = value;
   }
 
+  bool MayUseBluetooth(RenderFrameHost* frame) override {
+    return may_use_bluetooth_;
+  }
+
+  void set_may_use_bluetooth(bool value) { may_use_bluetooth_ = value; }
+
+  AllowWebBluetoothResult AllowWebBluetooth(
+      content::BrowserContext* browser_context,
+      const url::Origin& requesting_origin,
+      const url::Origin& embedding_origin) override {
+    return allow_web_bluetooth_;
+  }
+
+  void set_allow_web_bluetooth(AllowWebBluetoothResult value) {
+    allow_web_bluetooth_ = value;
+  }
+
   void RunBluetoothScanningPromptEventCallback(
       BluetoothScanningPrompt::Event event) {
     if (!prompt_) {
@@ -383,6 +400,9 @@
 
  private:
   bool has_device_permission_ = false;
+  bool may_use_bluetooth_ = true;
+  AllowWebBluetoothResult allow_web_bluetooth_ =
+      AllowWebBluetoothResult::kAllow;
   raw_ptr<FakeBluetoothScanningPrompt, AcrossTasksDanglingUntriaged> prompt_ =
       nullptr;
 };
@@ -694,6 +714,17 @@
         &service_ptr_.ExtractAsDangling()->render_frame_host());
   }
 
+  void ForgetDevice(
+      const blink::WebBluetoothDeviceId& device_id,
+      blink::mojom::WebBluetoothService::ForgetDeviceCallback callback) {
+    service_ptr_->ForgetDevice(device_id, std::move(callback));
+  }
+
+  std::string GetAllowedDeviceAddress(
+      const blink::WebBluetoothDeviceId& device_id) {
+    return service_ptr_->allowed_devices().GetDeviceAddress(device_id);
+  }
+
   scoped_refptr<FakeBluetoothAdapter> adapter_;
   raw_ptr<WebBluetoothServiceImpl> service_ptr_ = nullptr;
   mojo::Remote<blink::mojom::WebBluetoothService> service_;
@@ -1537,4 +1568,20 @@
   EXPECT_EQ(write_future.Get(), WebBluetoothResult::BLOCKLISTED_WRITE);
 }
 
+TEST_F(WebBluetoothServiceImplTest, ForgetDevice_NotAllowed) {
+  blink::WebBluetoothDeviceId device_id =
+      AddTestDevice(battery_device_bundle());
+  EXPECT_FALSE(GetAllowedDeviceAddress(device_id).empty());
+
+  browser_client_.bluetooth_delegate()->set_may_use_bluetooth(false);
+
+  base::test::TestFuture<void> future;
+  ForgetDevice(device_id, future.GetCallback());
+  EXPECT_TRUE(future.Wait());
+
+  // The device should STILL be in allowed devices because ForgetDevice should
+  // return early.
+  EXPECT_FALSE(GetAllowedDeviceAddress(device_id).empty());
+}
+
 }  // namespace content
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
index 4880d79..be04cdd 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -372,6 +372,23 @@
     has_device_permission_ = value;
   }
 
+  bool MayUseBluetooth(RenderFrameHost* frame) override {
+    return may_use_bluetooth_;
+  }
+
+  void set_may_use_bluetooth(bool value) { may_use_bluetooth_ = value; }
+
+  AllowWebBluetoothResult AllowWebBluetooth(
+      content::BrowserContext* browser_context,
+      const url::Origin& requesting_origin,
+      const url::Origin& embedding_origin) override {
+    return allow_web_bluetooth_;
+  }
+
+  void set_allow_web_bluetooth(AllowWebBluetoothResult value) {
+    allow_web_bluetooth_ = value;
+  }
+
   void RunBluetoothScanningPromptEventCallback(
       BluetoothScanningPrompt::Event event) {
     if (!prompt_) {
@@ -383,6 +400,9 @@
 
  private:
   bool has_device_permission_ = false;
+  bool may_use_bluetooth_ = true;
+  AllowWebBluetoothResult allow_web_bluetooth_ =
+      AllowWebBluetoothResult::kAllow;
   raw_ptr<FakeBluetoothScanningPrompt, AcrossTasksDanglingUntriaged> prompt_ =
       nullptr;
 };
@@ -694,6 +714,17 @@
         &service_ptr_.ExtractAsDangling()->render_frame_host());
   }
 
+  void ForgetDevice(
+      const blink::WebBluetoothDeviceId& device_id,
+      blink::mojom::WebBluetoothService::ForgetDeviceCallback callback) {
+    service_ptr_->ForgetDevice(device_id, std::move(callback));
+  }
+
+  std::string GetAllowedDeviceAddress(
+      const blink::WebBluetoothDeviceId& device_id) {
+    return service_ptr_->allowed_devices().GetDeviceAddress(device_id);
+  }
+
   scoped_refptr<FakeBluetoothAdapter> adapter_;
   raw_ptr<WebBluetoothServiceImpl> service_ptr_ = nullptr;
   mojo::Remote<blink::mojom::WebBluetoothService> service_;
@@ -1537,4 +1568,20 @@
   EXPECT_EQ(write_future.Get(), WebBluetoothResult::BLOCKLISTED_WRITE);
 }
 
+TEST_F(WebBluetoothServiceImplTest, ForgetDevice_NotAllowed) {
+  blink::WebBluetoothDeviceId device_id =
+      AddTestDevice(battery_device_bundle());
+  EXPECT_FALSE(GetAllowedDeviceAddress(device_id).empty());
+
+  browser_client_.bluetooth_delegate()->set_may_use_bluetooth(false);
+
+  base::test::TestFuture<void> future;
+  ForgetDevice(device_id, future.GetCallback());
+  EXPECT_TRUE(future.Wait());
+
+  // The device should STILL be in allowed devices because ForgetDevice should
+  // return early.
+  EXPECT_FALSE(GetAllowedDeviceAddress(device_id).empty());
+}
+
 }  // namespace content
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.