CVE-2026-13864
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc |
modified |
Files Changed
chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.ccchrome/browser/hid/hid_chooser_context.cc
Patch
From 63d25d353cf86ed8f6ac0082ac911c96634b2868 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <robpitkin@google.com>
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 <mattreynolds@chromium.org>
Commit-Queue: Rob Pitkin <robpitkin@chromium.org>
Reviewed-by: Colin Blundell <blundell@chromium.org>
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;
Regression Test / PoC
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
Original Bug Report
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.ccchrome/browser/hid/web_view_chooser_context.ccchrome/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.
- Attacker Setup: The attacker distributes a malicious application (e.g., an Isolated Web App or Chrome Extension) requesting
controlled-frameorwebviewpermissions. - 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 theWebHidAllowDevicesForUrlspolicy. This framing succeeds because GuestViews act as outermost main frames and bypassX-Frame-OptionsandCSP: frame-ancestors. - Script Injection: The attacker’s embedder application calls
executeScripton the guest element. For GuestViews,executeScriptunconditionally allows script injection into the guest without requiring standard extension host permissions for that origin. - Triggering WebHID: The injected script running in the guest context calls
navigator.hid.getDevices()anddevice.open(). - The Bypass: The browser processes the IPCs and calls
HidChooserContext::HasDevicePermission. Because the guest origin matches the enterprise policy, it returnstrueearly, completely ignoring the embedder’s lack of authorization. - 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.