CVE-2026-17986
Overview
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::WebBluetoothResultthat consolidates the Permissions-Policy check, theBluetoothDelegate::MayUseBluetooth()check, andAllowWebBluetooth()policy, returningSUCCESSonly 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.
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
- 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 toWebBluetoothServiceImpl. - Direct IPC call
A compromised or malicious renderer invokes
ForgetDevice(device_id, ...)directly over Mojo, bypassing any UI or higher-level gating. - Unauthorized state mutation
Pre-patch,
ForgetDeviceproceeds without checkingGetBluetoothAllowed()and removes the device from the origin’s allowed-devices store despite the policy prohibition. - Post-fix outcome
With the patch,
GetBluetoothAllowed()returns non-SUCCESS, the callback runs with no effect, and the device permission remains intact.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/bluetooth/web_bluetooth_service_impl_unittest.cc |
modified | |
TEST_Fcontent/browser/bluetooth/web_bluetooth_service_impl_unittest.cc |
modified |
Files Changed
content/browser/bluetooth/web_bluetooth_service_impl.cccontent/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
Audit Directions
- Entry-point check parityEnumerate 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 operationsPay 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 policyAudit callers of
BluetoothDelegate/AllowWebBluetooth-style delegates to ensure every renderer-reachable operation routes through the consolidated allow-check rather than assuming an upstream gate.
Patch
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
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 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