Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Sensor
DescriptionMissing authorization in Sensor
ComponentSensor
Bug ClassLogic Error
Tracker497017869
Fix commit7d7f3150edf7 (chromium/src) +431/-154
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
content/browser/generic_sensor/frame_sensor_provider_proxy.cc
modified

Files Changed

  • content/browser/generic_sensor/frame_sensor_provider_proxy.cc
From 7d7f3150edf76707accdc5ce07f0aa1c330cbb23 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <[email protected]>
Date: Mon, 06 Jul 2026 10:42:42 -0700
Subject: [PATCH] sensors: Supervisory control for generic sensors

Prior to this change, the browser process brokered a direct Mojo data
connection for Generic Sensors between the renderer and the Device
Service without retaining supervisory control. This could allow a
compromised renderer to bypass BFCache suspension, page visibility loss
gating, and permission revocation.

This change introduces the SensorClientController supervisory control
interface, brokered and tracked by FrameSensorProviderProxy in the
browser process. When a page is hidden, enters BFCache, or has its
sensor permissions revoked, Content broadcasts Suspend() or clears the
brokered sessions over this control pipe, ensuring the Device Service
pauses reading updates or tears down the sensor connection entirely.

services_unittests
gtest_filter="FrameSensorProviderProxyTest.*"
gtest_filter="GenericSensorServiceTest.*"

Bug: 497017869
Test: autoninja -C out/Default content_unittests device_unittests
Test: out/Default/content_unittests --
Test: out/Default/services_unittests --
Change-Id: Id4d53926f4434ffbb52083a71b6a8c981b92d689
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7919973
Reviewed-by: Daniel Cheng <[email protected]>
Commit-Queue: Rob Pitkin <[email protected]>
Reviewed-by: Matt Reynolds <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1657294}
---

diff --git a/content/browser/generic_sensor/frame_sensor_provider_proxy.cc b/content/browser/generic_sensor/frame_sensor_provider_proxy.cc
index d992e1f..c06f9f0 100644
--- a/content/browser/generic_sensor/frame_sensor_provider_proxy.cc
+++ b/content/browser/generic_sensor/frame_sensor_provider_proxy.cc
@@ -67,8 +67,9 @@
 
 FrameSensorProviderProxy::FrameSensorProviderProxy(
     RenderFrameHost* render_frame_host)
-    : DocumentUserData<FrameSensorProviderProxy>(render_frame_host) {
-  watcher_receivers_.set_disconnect_handler(
+    : DocumentUserData<FrameSensorProviderProxy>(render_frame_host),
+      WebContentsObserver(WebContents::FromRenderFrameHost(render_frame_host)) {
+  client_controllers_.set_disconnect_handler(
       base::BindRepeating(&FrameSensorProviderProxy::OnSensorDisconnect,
                           weak_factory_.GetWeakPtr()));
 }
@@ -84,7 +85,7 @@
   // Notify the delegate for each active connection that is being cleared.
   auto* delegate = GetContentClient()->browser()->GetSensorDelegate();
   if (delegate) {
-    for (size_t i = 0; i < watcher_receivers_.size(); ++i) {
+    for (size_t i = 0; i < client_controllers_.size(); ++i) {
       delegate->OnSensorStopped(&render_frame_host());
     }
   }
@@ -97,6 +98,7 @@
 
 void FrameSensorProviderProxy::OnMojoConnectionError() {
   receiver_set_.Clear();
+  client_controllers_.Clear();
 }
 
 void FrameSensorProviderProxy::GetSensor(device::mojom::SensorType type,
@@ -144,7 +146,7 @@
             WebContents::FromRenderFrameHost(&render_frame_host()));
 
     web_contents_sensor_provider->GetSensor(
-        type, mojo::NullRemote(),
+        type, mojo::NullReceiver(), /*initially_suspended=*/false,
         base::BindOnce(
             &FrameSensorProviderProxy::OnHardwareCheckForBlockedSensor,
             weak_factory_.GetWeakPtr(), type));
@@ -158,33 +160,37 @@
     scoped_observation_.Observe(web_contents_sensor_provider);
   }
 
-  if (base::FeatureList::IsEnabled(
-          features::kSeverSensorConnectionsOnPermissionRevocation) &&
-      !permission_subscription_id_) {
-    permission_subscription_id_ =
-        permission_controller->SubscribeToPermissionResultChange(
-            content::PermissionDescriptorUtil::
-                CreatePermissionDescriptorForPermissionType(
-                    blink::PermissionType::SENSORS),
-            nullptr, &render_frame_host(),
-            render_frame_host().GetLastCommittedOrigin().GetURL(),
-            /*should_include_device_status=*/false,
-            base::BindRepeating(&FrameSensorProviderProxy::OnPermissionChanged,
-                                weak_factory_.GetWeakPtr()));
-  }
+  mojo::PendingReceiver<device::mojom::SensorClientController>
+      controller_receiver;
+  mojo::PendingRemote<device::mojom::SensorClientController> controller;
+  bool initially_suspended = false;
 
-  // Create watcher but defer binding until success
-  mojo::PendingRemote<device::mojom::SensorConnectionWatcher> watcher;
-  mojo::PendingReceiver<device::mojom::SensorConnectionWatcher> receiver;
   if (ShouldTrackSensorConnection()) {
-    receiver = watcher.InitWithNewPipeAndPassReceiver();
+    if (!permission_subscription_id_) {
+      permission_subscription_id_ =
+          permission_controller->SubscribeToPermissionResultChange(
+              content::PermissionDescriptorUtil::
+                  CreatePermissionDescriptorForPermissionType(
+                      blink::PermissionType::SENSORS),
+              nullptr, &render_frame_host(),
+              render_frame_host().GetLastCommittedOrigin().GetURL(),
+              /*should_include_device_status=*/false,
+              base::BindRepeating(
+                  &FrameSensorProviderProxy::OnPermissionChanged,
+                  weak_factory_.GetWeakPtr()));
+    }
+
+    controller_receiver = controller.InitWithNewPipeAndPassReceiver();
+    if (base::FeatureList::IsEnabled(features::kSensorPrivacyMitigations)) {
+      initially_suspended = ShouldSuspendSensors();
+    }
   }
 
   web_contents_sensor_provider->GetSensor(
-      type, std::move(watcher),
+      type, std::move(controller_receiver), initially_suspended,
       base::BindOnce(&FrameSensorProviderProxy::OnHardwareCheckCompleted,
                      weak_factory_.GetWeakPtr(), type, permission_status,
-                     has_valid_gesture, std::move(receiver),
+                     has_valid_gesture, std::move(controller),
                      std::move(callback)));
 }
 
@@ -192,7 +198,7 @@
     device::mojom::SensorType type,
     blink::mojom::PermissionStatus permission_status,
     bool user_gesture,
-    mojo::PendingReceiver<device::mojom::SensorConnectionWatcher> receiver,
+    mojo::PendingRemote<device::mojom::SensorClientController> controller,
     GetSensorCallback callback,
     device::mojom::SensorCreationResult result,
     device::mojom::SensorInitParamsPtr params) {
@@ -207,7 +213,7 @@
   }
 
   if (permission_status == blink::mojom::PermissionStatus::GRANTED) {
-    FinalizeSensorConnection(std::move(receiver));
+    FinalizeSensorConnection(std::move(controller));
     std::move(callback).Run(result, std::move(params));
     return;
   }
@@ -227,7 +233,7 @@
                                    user_gesture),
       base::BindOnce(&FrameSensorProviderProxy::OnPermissionRequestCompleted,
                      weak_factory_.GetWeakPtr(), std::move(params),
-                     std::move(callback), std::move(receiver)));
+                     std::move(callback), std::move(controller)));
 }
 
 void FrameSensorProviderProxy::OnHardwareCheckForBlockedSensor(
@@ -256,7 +262,7 @@
 void FrameSensorProviderProxy::OnPermissionRequestCompleted(
     device::mojom::SensorInitParamsPtr params,
     GetSensorCallback callback,
-    mojo::PendingReceiver<device::mojom::SensorConnectionWatcher> receiver,
+    mojo::PendingRemote<device::mojom::SensorClientController> controller,
     PermissionResult permission_result) {
   if (permission_result.status != blink::mojom::PermissionStatus::GRANTED) {
     std::move(callback).Run(
@@ -264,7 +270,7 @@
     return;
   }
 
-  FinalizeSensorConnection(std::move(receiver));
+  FinalizeSensorConnection(std::move(controller));
 
   std::move(callback).Run(device::mojom::SensorCreationResult::SUCCESS,
                           std::move(params));
@@ -277,15 +283,49 @@
         GetContentClient()->browser()->GetSensorDelegate();
     if (delegate) {
       // Notify the delegate for each active connection that is being cleared.
-      for (size_t i = 0; i < watcher_receivers_.size(); ++i) {
+      for (size_t i = 0; i < client_controllers_.size(); ++i) {
         delegate->OnSensorStopped(&render_frame_host());
       }
     }
-    watcher_receivers_.Clear();
+    client_controllers_.Clear();
   }
 }
 
-void FrameSensorProviderProxy::OnSensorDisconnect() {
+void FrameSensorProviderProxy::OnVisibilityChanged(
+    content::Visibility visibility) {
+  UpdateSensorSessionControllers();
+}
+
+void FrameSensorProviderProxy::RenderFrameHostStateChanged(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc b/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc
index dfb3c42..fc8d0a5 100644
--- a/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc
+++ b/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc
@@ -259,5 +259,61 @@
   EXPECT_EQ(result, device::mojom::SensorCreationResult::ERROR_NOT_ALLOWED);
 }
 
+TEST_F(FrameSensorProviderProxyTest,
+       GetSensor_VisibilityChanged_SuspendsAndResumes) {
+  EXPECT_CALL(*permission_manager(),
+              GetPermissionResultForCurrentDocument(_, _, _))
+      .WillOnce(Return(PermissionResult(blink::mojom::PermissionStatus::GRANTED,
+                                        PermissionStatusSource::UNSPECIFIED)));
+
+  auto provider = GetWebSensorProvider();
+  static_cast<TestRenderFrameHost*>(main_test_rfh())->SimulateUserActivation();
+
+  mojo::Remote<device::mojom::Sensor> sensor_remote;
+  base::test::TestFuture<device::mojom::SensorCreationResult,
+                         device::mojom::SensorInitParamsPtr>
+      future;
+  provider->GetSensor(device::mojom::SensorType::ACCELEROMETER,
+                      /*user_gesture=*/true, future.GetCallback());
+  auto [result, params] = future.Take();
+  EXPECT_EQ(result, device::mojom::SensorCreationResult::SUCCESS);
+  sensor_remote.Bind(std::move(params->sensor));
+
+  device::FakeSensor* fake_sensor = fake_sensor_provider()->accelerometer();
+  ASSERT_TRUE(fake_sensor);
+
+  web_contents()->WasHidden();
+  EXPECT_TRUE(fake_sensor->WaitForBrowserSuspend(true));
+
+  web_contents()->WasShown();
+  EXPECT_TRUE(fake_sensor->WaitForBrowserSuspend(false));
+}
+
+TEST_F(FrameSensorProviderProxyTest,
+       GetSensor_InitiallyHidden_StartsSuspended) {
+  EXPECT_CALL(*permission_manager(),
+              GetPermissionResultForCurrentDocument(_, _, _))
+      .WillOnce(Return(PermissionResult(blink::mojom::PermissionStatus::GRANTED,
+                                        PermissionStatusSource::UNSPECIFIED)));
+
+  web_contents()->WasHidden();
+  auto provider = GetWebSensorProvider();
+  static_cast<TestRenderFrameHost*>(main_test_rfh())->SimulateUserActivation();
+
+  mojo::Remote<device::mojom::Sensor> sensor_remote;
+  base::test::TestFuture<device::mojom::SensorCreationResult,
+                         device::mojom::SensorInitParamsPtr>
+      future;
+  provider->GetSensor(device::mojom::SensorType::ACCELEROMETER,
+                      /*user_gesture=*/true, future.GetCallback());
+  auto [result, params] = future.Take();
+  EXPECT_EQ(result, device::mojom::SensorCreationResult::SUCCESS);
+  sensor_remote.Bind(std::move(params->sensor));
+
+  device::FakeSensor* fake_sensor = fake_sensor_provider()->accelerometer();
+  ASSERT_TRUE(fake_sensor);
+  EXPECT_TRUE(fake_sensor->is_browser_suspended());
+}
+
 }  // namespace
 }  // namespace content
diff --git a/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc b/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc
index 3ce1944..5daedf4b 100644
--- a/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc
+++ b/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc
@@ -137,11 +137,12 @@
 
   void GetSensor(
       device::mojom::SensorType type,
-      mojo::PendingRemote<device::mojom::SensorConnectionWatcher> watcher,
+      mojo::PendingReceiver<device::mojom::SensorClientController> controller,
+      bool initially_suspended,
       GetSensorCallback callback) override {
     std::move(interception_callback_).Run();
-    device::FakeSensorProvider::GetSensor(type, std::move(watcher),
-                                          std::move(callback));
+    device::FakeSensorProvider::GetSensor(
+        type, std::move(controller), initially_suspended, std::move(callback));
   }
 
  private:
@@ -202,7 +203,7 @@
   ASSERT_TRUE(fake_sensor);
 
   base::RunLoop run_loop;
-  fake_sensor->SetWatcherDisconnectCallback(run_loop.QuitClosure());
+  fake_sensor->SetControllerDisconnectCallback(run_loop.QuitClosure());
 
   // Trigger permission revocation.
   TestPermissionManager* permission_manager =
diff --git a/device/vr/test/fake_sensor_provider.cc b/device/vr/test/fake_sensor_provider.cc
index 09c2c7de..ac573989 100644
--- a/device/vr/test/fake_sensor_provider.cc
+++ b/device/vr/test/fake_sensor_provider.cc
@@ -29,7 +29,8 @@
 
 void FakeXRSensorProvider::GetSensor(
     mojom::SensorType type,
-    mojo::PendingRemote<mojom::SensorConnectionWatcher> watcher,
+    mojo::PendingReceiver<mojom::SensorClientController> controller,
+    bool initially_suspended,
     GetSensorCallback callback) {
   callback_ = std::move(callback);
 }
diff --git a/device/vr/test/fake_sensor_provider.h b/device/vr/test/fake_sensor_provider.h
index d82eff2..8736e35 100644
--- a/device/vr/test/fake_sensor_provider.h
+++ b/device/vr/test/fake_sensor_provider.h
@@ -8,7 +8,6 @@
 #include "device/vr/vr_export.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver.h"
-#include "services/device/public/mojom/sensor.mojom.h"
 #include "services/device/public/mojom/sensor_provider.mojom.h"
 
 namespace device {
@@ -24,9 +23,11 @@
   void CallCallback(mojom::SensorInitParamsPtr param);
 
   // device::mojom::SensorProvider overrides.
-  void GetSensor(mojom::SensorType type,
-                 mojo::PendingRemote<mojom::SensorConnectionWatcher> watcher,
-                 GetSensorCallback callback) override;
+  void GetSensor(
+      mojom::SensorType type,
+      mojo::PendingReceiver<mojom::SensorClientController> controller,
+      bool initially_suspended,
+      GetSensorCallback callback) override;
   void CreateVirtualSensor(
       mojom::SensorType type,
       mojom::VirtualSensorMetadataPtr metadata,
diff --git a/services/device/generic_sensor/generic_sensor_service_unittest.cc b/services/device/generic_sensor/generic_sensor_service_unittest.cc
index cc5d0cf2..83759ec 100644
--- a/services/device/generic_sensor/generic_sensor_service_unittest.cc
+++ b/services/device/generic_sensor/generic_sensor_service_unittest.cc
@@ -14,6 +14,7 @@
 #include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/test_future.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "services/device/device_service.h"
@@ -245,7 +246,8 @@
   auto client = std::make_unique<TestSensorClient>(SensorType::ACCELEROMETER);
   base::RunLoop run_loop;
   sensor_provider_->GetSensor(
-      SensorType::ACCELEROMETER, mojo::NullRemote(),
+      SensorType::ACCELEROMETER, mojo::NullReceiver(),
+      /*initially_suspended=*/false,
       base::BindOnce(&TestSensorClient::OnSensorCreated,
                      base::Unretained(client.get()), run_loop.QuitClosure()));
   run_loop.Run();
@@ -260,7 +262,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -285,7 +288,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::AMBIENT_LIGHT, mojo::NullRemote(),
+        SensorType::AMBIENT_LIGHT, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -307,7 +311,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::LINEAR_ACCELERATION, mojo::NullRemote(),
+        SensorType::LINEAR_ACCELERATION, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -329,11 +334,13 @@
     base::RunLoop run_loop;
     auto barrier_closure = base::BarrierClosure(2, run_loop.QuitClosure());
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client_1.get()), barrier_closure));
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client_2.get()), barrier_closure));
     run_loop.Run();
@@ -360,11 +367,13 @@
     base::RunLoop run_loop;
     auto barrier_closure = base::BarrierClosure(2, run_loop.QuitClosure());
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client_1.get()), barrier_closure));
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client_2.get()), barrier_closure));
     run_loop.Run();
@@ -389,7 +398,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -430,7 +440,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::AMBIENT_LIGHT, mojo::NullRemote(),
+        SensorType::AMBIENT_LIGHT, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -462,7 +473,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::AMBIENT_LIGHT, mojo::NullRemote(),
+        SensorType::AMBIENT_LIGHT, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -495,7 +507,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -541,11 +554,13 @@
     base::RunLoop run_loop;
     auto barrier_closure = base::BarrierClosure(2, run_loop.QuitClosure());
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client_1.get()), barrier_closure));
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client_2.get()), barrier_closure));
     run_loop.Run();
@@ -571,7 +586,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(client.get()), run_loop.QuitClosure()));
     run_loop.Run();
@@ -608,7 +624,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
-        SensorType::ACCELEROMETER, mojo::NullRemote(),
+        SensorType::ACCELEROMETER, mojo::NullReceiver(),
+        /*initially_suspended=*/false,
         base::BindOnce(&TestSensorClient::OnSensorCreated,
                        base::Unretained(new_client.get()),
                        run_loop.QuitClosure()));
@@ -664,7 +681,8 @@
   {
     base::RunLoop run_loop;
     sensor_provider_->GetSensor(
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Missing browser-side enforcement for Generic Sensor privacy mitigations

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The browser process brokers a direct Mojo connection for Generic Sensors between a renderer and the Device Service without retaining control of the pipe. This potentially allows a compromised renderer to bypass visibility/focus gating, BFCache suspension, and permission revocation. An attacker could thereby continuously monitor sensitive motion data in the background or across cross-origin navigations.

Affected files:

  • content/browser/generic_sensor/frame_sensor_provider_proxy.cc
  • content/browser/generic_sensor/web_contents_sensor_provider_proxy.cc
  • services/device/generic_sensor/sensor_provider_impl.cc
  • services/device/generic_sensor/sensor_impl.cc
  • third_party/blink/renderer/modules/sensor/sensor_proxy.cc

Estimated timestamp from git blame: 2023-10-18

Vulnerability Details

The Generic Sensor API implementation currently suffers from a structural flaw where the browser process fails to retain control over sensor pipes after they are brokered to a renderer.

When a renderer requests a sensor, content::FrameSensorProviderProxy::GetSensor() performs a one-time permission and policy check. Upon success, it simply forwards the renderer’s Mojo callback to the Device Service (services/device/generic_sensor/sensor_provider_impl.cc). The Device Service creates a device::SensorImpl instance, bounds it to a mojo::UniqueReceiverSet<mojom::Sensor>, and returns a direct mojom::Sensor pipe and a shared-memory region handle directly to the renderer.

Crucially, neither FrameSensorProviderProxy nor WebContentsSensorProviderProxy retains a reference to this mojom::Sensor pipe. This architectural hand-off leads to several severe privacy mitigation bypasses:

  1. Visibility and Focus Gating Bypass: The Generic Sensor specification requires that sensors only provide data to visible, focused frames. This is currently enforced entirely client-side by the renderer (blink::SensorProxy::ShouldSuspendUpdates()). The Device Service’s SensorImpl blindly trusts Resume() and Suspend() calls from the renderer. A compromised renderer can simply call Resume() and ignore visibility state.
  2. Permission Revocation Bypass: Because the browser proxy does not retain the brokered sensor pipes, it cannot close them if the user later revokes the site’s “Sensors” permission. The direct pipe to the Device Service remains active.
  3. Navigation and BFCache Bypass: When a document is destroyed or enters the BFCache, FrameSensorProviderProxy is destroyed (as it is a DocumentUserData). However, destroying the proxy only severs the brokering WebSensorProvider pipe. The actual SensorImpl in the Device Service remains fully alive because its lifetime is tied to the renderer holding the mojom::Sensor remote.

Suggested Attacker Steps

(Note: These are potential steps based on code analysis; our tooling agent does not yet have the ability to run code to produce a working PoC.)

  1. Achieve code execution within a sandboxed renderer process (e.g., via a separate V8 or Blink vulnerability).
  2. Construct a Mojo request to blink::mojom::WebSensorProvider::GetSensor() for a sensitive sensor like ACCELEROMETER.
  3. Because the page is active and motion sensors often default to allowed, the browser process’s initial permission check passes.
  4. Receive the device::mojom::SensorInitParams response, which contains a direct mojom::Sensor remote to the Device Service and a read-only handle to the sensor’s shared memory buffer.
  5. Call the Resume() method on the mojom::Sensor remote to instruct the Device Service to start polling the hardware.
  6. Intentionally ignore any backgrounding, visibility loss, or focus loss events. Do not call Suspend() on the remote.
  7. Continue to read high-resolution motion data from the shared memory buffer indefinitely, even after the user navigates away or revokes permissions, enabling persistent side-channel attacks (e.g., keystroke inference).

Suggested Fix

To resolve this, the browser process must act as the authoritative gatekeeper for the lifetime and state of active sensors:

  1. Active Proxying or Tracking: FrameSensorProviderProxy should either actively proxy the device::mojom::Sensor interface (intercepting Suspend/Resume and sensor data notifications) or track the returned pipes so they can be forcefully disconnected.
  2. Browser-side State Enforcement: The browser process should independently monitor the WebContents visibility, focus state, and PermissionController status. When a frame loses visibility/focus, or if permissions are revoked, the browser should forcefully suspend or disconnect the tracked sensor pipes, rather than relying on the untrusted renderer to do so voluntarily.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker