Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Core
DescriptionMissing authorization in Core
ComponentCore
Bug ClassAuthorization Error
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.
Tracker502452118
Fix commit0488f340b540 (chromium/src) +88/-14
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-14 (per vendor advisory)
Fix landed2026-07-31 (public git)
Disclosed2026-09-08
Patch gap39 days — fix public in source before the advisory

Background

Fenced frame
an embedded frame type whose interaction with its embedder is deliberately restricted for privacy, so certain web APIs must be blocked inside it.
`BrowserInterfaceBroker`
the per-frame browser-process endpoint (blink::mojom::BrowserInterfaceBroker) through which a renderer requests Mojo interfaces such as device::mojom::VibrationManager.
Interface binder
a browser-process callback registered in the binder map (content/browser/browser_interface_binders.cc) that receives a mojo::PendingReceiver from a renderer request and connects it to the backing service.
`bad_message::ReceivedBadMessage`
the mechanism that terminates a renderer process when it sends an illegitimate IPC, recording a reason enum from content/browser/bad_message.h.

Root Cause Analysis

The renderer-side guard in third_party/blink/renderer/modules/vibration/vibration_controller.cc blocked navigator.vibrate() inside a fenced-frame tree, but this check lived entirely in the untrusted renderer. The browser-process binder for device::mojom::VibrationManager, registered in browser_interface_binders.cc, bound the receiver to the device service unconditionally via GetDeviceService().BindVibrationManager(...) without ever consulting the frame’s fenced-frame status. A renderer could therefore request the VibrationManager interface directly on its BrowserInterfaceBroker, bypassing the JavaScript-level check and obtaining a working interface the fenced-frame policy was supposed to deny — a missing browser-side authorization check.

The fix factors the inline lambda into a named BindVibrationManager() that calls host->IsNestedWithinFencedFrame() and, when true, invokes bad_message::ReceivedBadMessage with the new BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME reason and returns before binding, mirroring the adjacent BindBatteryMonitor() pattern. The check now runs in the trusted browser process, so the fenced-frame restriction is enforced regardless of what the renderer does.

Key insight
The single core mistake was enforcing the fenced-frame restriction only in the renderer (which an attacker controls) while the browser-process binder trusted the request unconditionally; the fix relocates the authorization check into the browser-side binder so it cannot be bypassed.

Attack Path

  1. Load a fenced frame The attacker embeds a fenced frame (or controls a document inside one), a context in which navigator.vibrate() is meant to be blocked.
  2. Bypass the renderer guard Instead of calling the JavaScript API, the renderer requests device::mojom::VibrationManager directly through its BrowserInterfaceBroker (broker->GetInterface(...)), as the shipped test VibrationManagerBindRejectedInFencedFrame demonstrates against the pre-patch binder.
  3. Browser binds the interface Before the fix, the binder lambda called GetDeviceService().BindVibrationManager(...) with no fenced-frame check, connecting the fenced frame to the vibration service that policy intended to deny.
  4. Use of the disallowed capability With a live VibrationManager, the fenced frame can drive device vibration and any privacy/side-channel or user-annoyance behavior that access enables (inferred) — the patch itself only shows an authorization check being added, not a memory-safety sink.

Impact Assessment

An attacker-controlled document inside a fenced frame gains access to the device::mojom::VibrationManager browser-process interface that the fenced-frame policy is designed to withhold, defeating a privacy/isolation restriction rather than corrupting memory. The precondition is the ability to run script in a fenced-frame tree and issue a raw Mojo interface request. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. Any escalation beyond obtaining the disallowed interface is (inferred).

Files Changed

  • tools/metrics/histograms/metadata/stability/enums.xml
  • content/browser/bad_message.h
  • content/browser/browser_interface_binders.cc
  • third_party/blink/renderer/modules/vibration/vibration_controller.cc
  • content/browser/security_exploit_browsertest.cc

Audit Directions

  • Renderer-only guards on privileged capabilities
    Wherever an API is blocked only in Blink (e.g. a IsInFencedFrameTree() / policy check in a renderer-side controller), verify the corresponding browser-process binder in browser_interface_binders.cc independently enforces the same restriction, since renderers are untrusted.
  • Interface binders lacking a frame-context check
    Audit map->Add<...> entries whose lambdas call GetDeviceService() or similar service getters without consulting IsNestedWithinFencedFrame() or other frame-state predicates; compare each against the guarded BindBatteryMonitor() / BindVibrationManager() pattern.
  • Fenced-frame and prerender API surface
    Systematically enumerate web-exposed device and sensor interfaces (vibration, battery, orientation, pressure, geolocation) and confirm each browser-side binder rejects binds from fenced-frame or other restricted browsing contexts with a distinct bad_message reason.
From 0488f340b54088e3ecf978b6a2ff7227ccb187de Mon Sep 17 00:00:00 2001
From: Shunya Shishido <sisidovski@chromium.org>
Date: Thu, 30 Jul 2026 22:58:59 -0700
Subject: [PATCH] Reject VibrationManager bind requests from fenced frames

The renderer already refuses navigator.vibrate() inside a fenced-frame
tree, but the browser-side device::mojom::VibrationManager binder did
not enforce the same restriction. Mirror the adjacent
BindBatteryMonitor() pattern: factor the lambda into
BindVibrationManager(), check IsNestedWithinFencedFrame(), and terminate
the renderer with the new BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME
reason instead of binding the receiver to the device service.

In addition, update VibrationController::vibrate() in Blink to check
IsInFencedFrameTree() before calling From(navigator). This prevents
VibrationController instantiation and unconditional Mojo interface
binding in fenced frames, avoiding unexpected renderer terminations
during web platform tests (navigator-vibrate.https.html).

Add a content_browsertest that requests the interface directly on a
fenced frame's BrowserInterfaceBroker and asserts the renderer is
terminated with the new bad-message reason.

TAG=agy
CONV=0f7e37f2-2d45-401e-9bf7-0a3ac2b8fc8e

Bug: 502452118
Change-Id: Ic81c2d205150577c13c50f8bcd4d4328145012a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8173483
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Commit-Queue: Shunya Shishido <sisidovski@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671637}
---
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 8fa45ea3..baddc1a5 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -394,6 +394,7 @@
   RFH_INVALID_CONNECTION_ALLOWLIST_ATTRIBUTE = 366,
   PMM_UNSUBSCRIBE_IN_FENCED_FRAME = 367,
   PMM_GET_SUBSCRIPTION_IN_FENCED_FRAME = 368,
+  BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME = 369,
 
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc
index 94c73d9..6f080e0 100644
--- a/content/browser/browser_interface_binders.cc
+++ b/content/browser/browser_interface_binders.cc
@@ -741,6 +741,20 @@
   GetDeviceService().BindBatteryMonitor(std::move(receiver));
 }
 
+void BindVibrationManager(
+    RenderFrameHost* host,
+    mojo::PendingReceiver<device::mojom::VibrationManager> receiver) {
+  if (host->IsNestedWithinFencedFrame()) {
+    bad_message::ReceivedBadMessage(
+        host->GetProcess(), bad_message::BadMessageReason::
+                                BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME);
+    return;
+  }
+  GetDeviceService().BindVibrationManager(
+      std::move(receiver), static_cast<RenderFrameHostImpl*>(host)
+                               ->CreateVibrationManagerListener());
+}
+
 #if BUILDFLAG(ENABLE_COMPUTE_PRESSURE)
 void BindPressureManager(
     RenderFrameHost* host,
@@ -1434,13 +1448,7 @@
   map->Add<blink::mojom::AnchorElementInteractionHost>(
       &AnchorElementInteractionHostImpl::Create);
 
-  map->Add<device::mojom::VibrationManager>(
-      [](RenderFrameHost* host,
-         mojo::PendingReceiver<device::mojom::VibrationManager> receiver) {
-        GetDeviceService().BindVibrationManager(
-            std::move(receiver), static_cast<RenderFrameHostImpl*>(host)
-                                     ->CreateVibrationManagerListener());
-      });
+  map->Add<device::mojom::VibrationManager>(&BindVibrationManager);
 
 #if BUILDFLAG(IS_CHROMEOS)
   if (base::FeatureList::IsEnabled(features::kWebLockScreenApi)) {
diff --git a/third_party/blink/renderer/modules/vibration/vibration_controller.cc b/third_party/blink/renderer/modules/vibration/vibration_controller.cc
index 9950d048..3ba3da13 100644
--- a/third_party/blink/renderer/modules/vibration/vibration_controller.cc
+++ b/third_party/blink/renderer/modules/vibration/vibration_controller.cc
@@ -108,6 +108,15 @@
   // reference to |window| or |navigator| was retained in another window.
   if (!navigator.DomWindow())
     return false;
+
+  LocalFrame* frame = navigator.DomWindow()->GetFrame();
+  if (frame && frame->IsInFencedFrameTree()) {
+    Intervention::GenerateReport(
+        frame, "NavigatorVibrate",
+        "Blocked call to navigator.vibrate inside a fenced frame.");
+    return false;
+  }
+
   return From(navigator).Vibrate(pattern);
 }
 
@@ -133,13 +142,6 @@
   UseCounter::Count(DomWindow(), WebFeature::kNavigatorVibrate);
 
   LocalFrame* frame = DomWindow()->GetFrame();
-  if (frame->IsInFencedFrameTree()) {
-    Intervention::GenerateReport(
-        frame, "NavigatorVibrate",
-        "Blocked call to navigator.vibrate inside a fenced frame.");
-    return false;
-  }
-
   if (!frame->GetPage()->IsPageVisible())
     return false;
 
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index 47d0566..dc2dc4e3 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -544,6 +544,7 @@
   <int value="366" label="RFH_INVALID_CONNECTION_ALLOWLIST_ATTRIBUTE"/>
   <int value="367" label="PMM_UNSUBSCRIBE_IN_FENCED_FRAME"/>
   <int value="368" label="PMM_GET_SUBSCRIPTION_IN_FENCED_FRAME"/>
+  <int value="369" label="BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME"/>
 </enum>
 
 <enum name="BadMessageReasonExtensions">
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 2a3793a..c1d86a44 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -102,7 +102,9 @@
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/test/embedded_test_server/http_request.h"
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/device/public/mojom/battery_monitor.mojom.h"
 #include "services/device/public/mojom/screen_orientation.mojom.h"
+#include "services/device/public/mojom/vibration_manager.mojom.h"
 #include "services/network/public/cpp/network_switches.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/resource_request_body.h"
@@ -4175,6 +4177,66 @@
       << handle->GetInitiatorOrigin()->Serialize();
 }
 
+// Verify that binding VibrationManager is rejected when requested from within a
+// fenced frame.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+                       VibrationManagerBindRejectedInFencedFrame) {
+  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  GURL fenced_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  RenderFrameHost* fenced_rfh_generic =
+      fenced_frame_test_helper().CreateFencedFrame(
+          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
+  ASSERT_TRUE(fenced_rfh_generic);
+  RenderFrameHostImpl* fenced_rfh =
+      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());
+
+  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
+      fenced_rfh->browser_interface_broker_receiver_for_testing();
+  blink::mojom::BrowserInterfaceBroker* broker =
+      broker_receiver.internal_state()->impl();
+
+  mojo::Remote<device::mojom::VibrationManager> remote;
+  broker->GetInterface(remote.BindNewPipeAndPassReceiver());
+
+  EXPECT_EQ(bad_message::BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME,
+            kill_waiter.Wait());
+}
+
+// Verify that binding BatteryMonitor is rejected when requested from within a
+// fenced frame.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+                       BatteryMonitorBindRejectedInFencedFrame) {
+  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  GURL fenced_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  RenderFrameHost* fenced_rfh_generic =
+      fenced_frame_test_helper().CreateFencedFrame(
+          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
+  ASSERT_TRUE(fenced_rfh_generic);
+  RenderFrameHostImpl* fenced_rfh =
+      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());
+
+  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
+      fenced_rfh->browser_interface_broker_receiver_for_testing();
+  blink::mojom::BrowserInterfaceBroker* broker =
+      broker_receiver.internal_state()->impl();
+
+  mojo::Remote<device::mojom::BatteryMonitor> remote;
+  broker->GetInterface(remote.BindNewPipeAndPassReceiver());
+
+  EXPECT_EQ(bad_message::BIBI_BIND_BATTERY_MONITOR_FOR_FENCED_FRAME,
+            kill_waiter.Wait());
+}
+
 namespace {
 
 // Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 2a3793a..c1d86a44 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -102,7 +102,9 @@
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/test/embedded_test_server/http_request.h"
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/device/public/mojom/battery_monitor.mojom.h"
 #include "services/device/public/mojom/screen_orientation.mojom.h"
+#include "services/device/public/mojom/vibration_manager.mojom.h"
 #include "services/network/public/cpp/network_switches.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/resource_request_body.h"
@@ -4175,6 +4177,66 @@
       << handle->GetInitiatorOrigin()->Serialize();
 }
 
+// Verify that binding VibrationManager is rejected when requested from within a
+// fenced frame.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+                       VibrationManagerBindRejectedInFencedFrame) {
+  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  GURL fenced_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  RenderFrameHost* fenced_rfh_generic =
+      fenced_frame_test_helper().CreateFencedFrame(
+          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
+  ASSERT_TRUE(fenced_rfh_generic);
+  RenderFrameHostImpl* fenced_rfh =
+      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());
+
+  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
+      fenced_rfh->browser_interface_broker_receiver_for_testing();
+  blink::mojom::BrowserInterfaceBroker* broker =
+      broker_receiver.internal_state()->impl();
+
+  mojo::Remote<device::mojom::VibrationManager> remote;
+  broker->GetInterface(remote.BindNewPipeAndPassReceiver());
+
+  EXPECT_EQ(bad_message::BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME,
+            kill_waiter.Wait());
+}
+
+// Verify that binding BatteryMonitor is rejected when requested from within a
+// fenced frame.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+                       BatteryMonitorBindRejectedInFencedFrame) {
+  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  GURL fenced_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  RenderFrameHost* fenced_rfh_generic =
+      fenced_frame_test_helper().CreateFencedFrame(
+          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
+  ASSERT_TRUE(fenced_rfh_generic);
+  RenderFrameHostImpl* fenced_rfh =
+      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());
+
+  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
+      fenced_rfh->browser_interface_broker_receiver_for_testing();
+  blink::mojom::BrowserInterfaceBroker* broker =
+      broker_receiver.internal_state()->impl();
+
+  mojo::Remote<device::mojom::BatteryMonitor> remote;
+  broker->GetInterface(remote.BindNewPipeAndPassReceiver());
+
+  EXPECT_EQ(bad_message::BIBI_BIND_BATTERY_MONITOR_FOR_FENCED_FRAME,
+            kill_waiter.Wait());
+}
+
 namespace {
 
 // Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
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.