Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in WebHID
DescriptionInsufficient policy enforcement in WebHID
ComponentWebHID
Bug ClassLogic Error
Tracker496399913
Fix commit63d25d353cf8 (chromium/src) +52/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
modified

Files Changed

  • chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
  • chrome/browser/hid/hid_chooser_context.cc
From 63d25d353cf86ed8f6ac0082ac911c96634b2868 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <[email protected]>
Date: Mon, 11 May 2026 11:33:34 -0700
Subject: [PATCH] Fix WebHID enterprise policy bypass in GuestViews

This CL addresses a security vulnerability where GuestViews (such as
<webview> or <controlledframe>) could bypass isolation checks and access
HID devices granted to an origin by enterprise policy.

Previously, `HidChooserContext::HasDevicePermission` checked enterprise
policies before checking if the request originated from a GuestView. If
the guest origin was allowed by policy, access was granted immediately,
skipping the check in `WebViewChooserContext`. This allowed a malicious
host app to embed a trusted origin and hijack its HID access.

This CL modifies `HidChooserContext::HasDevicePermission` to skip the
enterprise policy check for guest requests, ensuring they always fall
through to `WebViewChooserContext` for proper mediation by the embedder.

Bug: 496399913
Change-Id: Id33492558bc5258c97884aaed37dd5ebedef5c4d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7824151
Reviewed-by: Matt Reynolds <[email protected]>
Commit-Queue: Rob Pitkin <[email protected]>
Reviewed-by: Colin Blundell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1628703}
---

diff --git a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
index 226be48..436aecf 100644
--- a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
@@ -6,9 +6,12 @@
 #include <string>
 #include <vector>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/test_future.h"
+#include "base/test/values_test_util.h"
+#include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_content_browser_client.h"
 #include "chrome/browser/controlled_frame/controlled_frame_permission_request_test_base.h"
 #include "chrome/browser/hid/chrome_hid_delegate.h"
@@ -16,9 +19,11 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/hid/hid_chooser_controller.h"
+#include "chrome/common/pref_names.h"
 #include "components/content_settings/core/common/content_settings_types.h"
 #include "components/download/public/common/download_item.h"
 #include "components/permissions/mock_chooser_controller_view.h"
+#include "components/prefs/pref_service.h"
 #include "content/public/browser/download_manager.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
@@ -456,4 +461,50 @@
                            return info.param.name;
                          });
 
+IN_PROC_BROWSER_TEST_P(ControlledFramePermissionRequestWebHidTest,
+                       PolicyGrantDoesNotBypassEmbedderForGuest) {
+  // 1. Install and open IWA, then create ControlledFrame.
+  auto [app_frame, controlled_frame] =
+      InstallAndOpenIwaThenCreateControlledFrame(
+          /*controlled_frame_host_name=*/std::nullopt, "/empty.html");
+  ASSERT_TRUE(app_frame);
+  ASSERT_TRUE(controlled_frame);
+
+  url::Origin guest_origin = controlled_frame->GetLastCommittedOrigin();
+
+  // 2. Set up policy allowing the guest origin.
+  // Note: vendor_id 0 and product_id 0 match the default device added in
+  // ControlledFramePermissionRequestWebHidTest::SetUpOnMainThread.
+  const char kPolicySetting[] = R"(
+      [
+        {
+          "devices": [{ "vendor_id": 0, "product_id": 0 }],
+          "urls": ["%s"]
+        }
+      ])";
+  g_browser_process->local_state()->Set(
+      prefs::kManagedWebHidAllowDevicesForUrls,
+      base::test::ParseJson(base::StringPrintf(
+          kPolicySetting, guest_origin.Serialize().c_str())));
+
+  // 3. Guest calls getDevices() and should NOT see the device because it needs
+  // embedder delegation.
+  constexpr char kTestScript[] = R"(
+    (async function() {
+      try {
+        const devices = await navigator.hid.getDevices();
+        if (devices.length === 0) {
+          return 'SUCCESS: NO_DEVICES';
+        }
+        return 'FAIL: Got ' + devices.length + ' devices';
+      } catch (err) {
+        return 'FAIL: ' + err.name + ': ' + err.message;
+      }
+    })();
+  )";
+
+  EXPECT_EQ("SUCCESS: NO_DEVICES",
+            content::EvalJs(controlled_frame, kTestScript).ExtractString());
+}
+
 }  // namespace controlled_frame
diff --git a/chrome/browser/hid/hid_chooser_context.cc b/chrome/browser/hid/hid_chooser_context.cc
index 88d3d33b..889dfc8 100644
--- a/chrome/browser/hid/hid_chooser_context.cc
+++ b/chrome/browser/hid/hid_chooser_context.cc
@@ -481,7 +481,7 @@
     }
   }
 
-  if (CanApplyPolicy() &&
+  if (CanApplyPolicy() && !embedding_origin_of_web_view &&
       HidPolicyAllowedDevicesFactory::GetForProfile(profile_)
           ->HasDevicePermission(origin, device)) {
     return true;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
index 226be48..436aecf 100644
--- a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
@@ -6,9 +6,12 @@
 #include <string>
 #include <vector>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/test_future.h"
+#include "base/test/values_test_util.h"
+#include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_content_browser_client.h"
 #include "chrome/browser/controlled_frame/controlled_frame_permission_request_test_base.h"
 #include "chrome/browser/hid/chrome_hid_delegate.h"
@@ -16,9 +19,11 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/hid/hid_chooser_controller.h"
+#include "chrome/common/pref_names.h"
 #include "components/content_settings/core/common/content_settings_types.h"
 #include "components/download/public/common/download_item.h"
 #include "components/permissions/mock_chooser_controller_view.h"
+#include "components/prefs/pref_service.h"
 #include "content/public/browser/download_manager.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
@@ -456,4 +461,50 @@
                            return info.param.name;
                          });
 
+IN_PROC_BROWSER_TEST_P(ControlledFramePermissionRequestWebHidTest,
+                       PolicyGrantDoesNotBypassEmbedderForGuest) {
+  // 1. Install and open IWA, then create ControlledFrame.
+  auto [app_frame, controlled_frame] =
+      InstallAndOpenIwaThenCreateControlledFrame(
+          /*controlled_frame_host_name=*/std::nullopt, "/empty.html");
+  ASSERT_TRUE(app_frame);
+  ASSERT_TRUE(controlled_frame);
+
+  url::Origin guest_origin = controlled_frame->GetLastCommittedOrigin();
+
+  // 2. Set up policy allowing the guest origin.
+  // Note: vendor_id 0 and product_id 0 match the default device added in
+  // ControlledFramePermissionRequestWebHidTest::SetUpOnMainThread.
+  const char kPolicySetting[] = R"(
+      [
+        {
+          "devices": [{ "vendor_id": 0, "product_id": 0 }],
+          "urls": ["%s"]
+        }
+      ])";
+  g_browser_process->local_state()->Set(
+      prefs::kManagedWebHidAllowDevicesForUrls,
+      base::test::ParseJson(base::StringPrintf(
+          kPolicySetting, guest_origin.Serialize().c_str())));
+
+  // 3. Guest calls getDevices() and should NOT see the device because it needs
+  // embedder delegation.
+  constexpr char kTestScript[] = R"(
+    (async function() {
+      try {
+        const devices = await navigator.hid.getDevices();
+        if (devices.length === 0) {
+          return 'SUCCESS: NO_DEVICES';
+        }
+        return 'FAIL: Got ' + devices.length + ' devices';
+      } catch (err) {
+        return 'FAIL: ' + err.name + ': ' + err.message;
+      }
+    })();
+  )";
+
+  EXPECT_EQ("SUCCESS: NO_DEVICES",
+            content::EvalJs(controlled_frame, kTestScript).ExtractString());
+}
+
 }  // namespace controlled_frame
Loading diff…

Original Bug Report

reported by [email protected]

Potential WebHID enterprise policy bypass via GuestView/ControlledFrame

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

Overview: A logic error in HidChooserContext::HasDevicePermission allows GuestViews to bypass isolation checks and access HID devices granted to an origin by enterprise policy. Because the policy check occurs before the GuestView embedder check, an untrusted embedder can frame a policy-authorized origin and inject scripts to hijack its HID access. This potentially enables unauthorized access to enterprise-restricted hardware without user interaction.

Affected files:

  • chrome/browser/hid/hid_chooser_context.cc
  • chrome/browser/hid/web_view_chooser_context.cc
  • chrome/browser/hid/hid_policy_allowed_devices.cc

Estimated timestamp from git blame: 2023-06-14

Summary

There is a potential cross-origin confused-deputy privilege escalation in Chrome’s WebHID implementation. A logic flaw in HidChooserContext::HasDevicePermission allows origins authorized by enterprise HID policies to access HID devices even when they are running within a restricted GuestView context (such as <webview> or <controlledframe>).

This bypasses the isolation mechanism intended to require the embedder of the GuestView to possess the permission and explicitly delegate it to the guest context.

Code Analysis

In chrome/browser/hid/hid_chooser_context.cc, the function HasDevicePermission determines if a context has permission to access a specific HID device. The implementation currently checks for an enterprise policy match before checking for GuestView embedder isolation:

484:   if (CanApplyPolicy() &&
485:       HidPolicyAllowedDevicesFactory::GetForProfile(profile_)
486:           ->HasDevicePermission(origin, device)) {
487:     return true;  // <--- Early return bypasses GuestView checks
488:   }
...
490:   if (!CanRequestObjectPermission(origin))
491:     return false;
492: 
493:   if (embedding_origin_of_web_view) {
494:     return web_view_chooser_context_.HasDevicePermission(
495:         origin, *embedding_origin_of_web_view, device);
496:   }

When a request originates from a GuestView, the origin parameter is the guest’s origin, and embedding_origin_of_web_view is the untrusted embedder’s origin. If an enterprise policy (such as WebHidAllowDevicesForUrls) grants permission to the guest’s origin, line 487 returns true immediately.

This skips the logic at line 493, which is intended to enforce the WebViewChooserContext invariant: a guest should only have access if the embedder also has permission and has specifically granted it to the guest.

Potential Attack Steps

Note: These are suggested steps based on static analysis, as our setup does not yet have the ability to run code or construct a live PoC.

  1. Attacker Setup: The attacker distributes a malicious application (e.g., an Isolated Web App or Chrome Extension) requesting controlled-frame or webview permissions.
  2. Framing the Target: The application uses <webview> or <controlledframe> to embed a target enterprise origin (e.g., https://internal.corp) known to be authorized for sensitive HID access via the WebHidAllowDevicesForUrls policy. This framing succeeds because GuestViews act as outermost main frames and bypass X-Frame-Options and CSP: frame-ancestors.
  3. Script Injection: The attacker’s embedder application calls executeScript on the guest element. For GuestViews, executeScript unconditionally allows script injection into the guest without requiring standard extension host permissions for that origin.
  4. Triggering WebHID: The injected script running in the guest context calls navigator.hid.getDevices() and device.open().
  5. The Bypass: The browser processes the IPCs and calls HidChooserContext::HasDevicePermission. Because the guest origin matches the enterprise policy, it returns true early, completely ignoring the embedder’s lack of authorization.
  6. Exfiltration: The injected script now has live I/O capabilities with the enterprise-restricted HID device and proxies this data back to the attacker’s embedder application (e.g., via postMessage).

Suggested Fix

To resolve this issue, the embedder validation must take precedence over or strictly encompass the guest’s policy grants.

The if (embedding_origin_of_web_view) block in HidChooserContext::HasDevicePermission (lines 493-496) should be moved above the enterprise policy check. Alternatively, if enterprise policies are intended to apply within GuestViews, the policy check must be modified to verify that the embedding_origin_of_web_view is also authorized by the policy or otherwise explicitly permitted to embed the privileged guest.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results 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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker
Links in the report