Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in USB
DescriptionIncorrect authorization in USB
ComponentUSB
Sub-componentUSB
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.
Tracker524520965
Fix commitb334f9d5a8d0 (chromium/src) +105/-37
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
Creditedhongan
Reported2026-06-16 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-08-25
Patch gap49 days — fix public in source before the advisory

Background

WebUSB blocklist
Chrome policy that denies web-originated USB control transfers targeting protected interface classes (e.g. HID, smartcard, audio) to prevent sites from driving sensitive devices.
Control transfer recipient
the wIndex/bmRequestType field selecting whether a USB CLASS request targets the whole DEVICE, an INTERFACE, an ENDPOINT, or OTHER, represented by UsbControlTransferRecipient.
`HasProtectedInterface`
helper that returns true when the active USB config contains any interface belonging to a blocklisted class.
`wIndex` routing quirk
many devices ignore the interface number in wIndex for device-level requests and route them to the currently active (possibly protected) interface.

Root Cause Analysis

In DeviceImpl::HasControlTransferPermission in services/device/usb/mojo/device_impl.cc, CLASS requests with recipient DEVICE or OTHER were authorized by looking up the interface named by index & 0xff via FindInterface. If that lookup resolved to a valid, non-protected interface, the request was allowed; the code only fell through to HasProtectedInterface(config) when the interface lookup failed (!interface). The invariant that should have held — a device-level CLASS request cannot reach a protected interface — was violated because a site could name any non-protected interface in wIndex while the device silently routed the transfer to its active protected interface.

The fix restructures the branch so that only recipients INTERFACE and ENDPOINT are resolved to a specific interface; for DEVICE and OTHER, the request is now blocked whenever HasProtectedInterface(config) is true, regardless of the wIndex value.

Key insight
The core mistake was trusting the caller-supplied wIndex to identify the true target of a device-level CLASS request, when devices that ignore wIndex route it to the active protected interface; the fix removes the per-wIndex allowance and blocks all DEVICE/OTHER CLASS requests whenever the configuration has any protected interface.

Attack Path

  1. Establish access A page obtains a WebUSB handle to a device whose active configuration contains at least one protected (blocklisted-class) interface alongside a non-protected interface.
  2. Craft a device-level CLASS request The site issues a ControlTransferIn/Out with type = CLASS and recipient = DEVICE (or OTHER).
  3. Spoof a benign interface in `wIndex` It sets index to a valid, non-protected interface number so FindInterface resolves to an allowed interface and FindBlockedClass returns nothing.
  4. Bypass the blocklist HasControlTransferPermission allows the transfer because the !interface guard on HasProtectedInterface(config) is never reached, yet the device routes the request to its active protected interface.

Impact Assessment

An attacker-controlled web page gains the ability to send CLASS control transfers to a protected USB interface it should never reach, defeating the WebUSB blocklist that guards sensitive device classes. The bypass runs in the browser/renderer-originated WebUSB path handled by the device service process. Preconditions are that the user has granted the site a WebUSB device permission and that the device both exposes a protected interface and ignores wIndex for device-level requests.

Changed Functions

FunctionChangeNotes
if
services/device/usb/mojo/device_impl.cc
modified
TEST_F
services/device/usb/mojo/device_impl_unittest.cc
modified

Files Changed

  • services/device/usb/mojo/device_impl_unittest.cc
  • services/device/usb/mojo/device_impl.cc

Audit Directions

  • Trusting client-supplied selectors for authorization
    Audit any permission check that resolves a caller-provided index, handle, or name to decide access, and verify the underlying device or subsystem actually honors that selector rather than routing to a different target.
  • Fail-open fallthroughs on lookup failure
    Look for authorization logic that only tightens (e.g. checks a global protected-state flag) in the !found branch, since the found-but-mismatched case can leave a bypass.
  • Recipient/scope conflation in transport protocols
    Review other USB/HID/Bluetooth control paths where a device-scoped operation is authorized as if it were narrowly scoped, especially where hardware ignores fine-grained routing fields.
From b334f9d5a8d0a54b39b0f17aa2149cdb3fd7feab Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Tue, 07 Jul 2026 16:52:07 -0700
Subject: [PATCH] usb: Block device-level CLASS requests if any interface is protected

WebUSB blocklist enforcement for device-level CLASS requests could be
bypassed by specifying a valid, non-protected interface index in the
wIndex field. For devices that ignore wIndex and route all device-level
requests to the active interface (which might be protected), this
allowed bypassing the protection.

This change blocks all device-level CLASS requests (recipient DEVICE or
OTHER) if the device configuration contains any protected interface,
regardless of the wIndex value.

Change-Id: Ia9cbd87f346c5938695809f4c0f3bfd43cb093de
Bug: 524520965
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7963404
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1658378}
---
diff --git a/services/device/usb/mojo/device_impl.cc b/services/device/usb/mojo/device_impl.cc
index b5df00a..9b3492bd 100644
--- a/services/device/usb/mojo/device_impl.cc
+++ b/services/device/usb/mojo/device_impl.cc
@@ -311,39 +311,34 @@
   // 2. CLASS Requests
   // ==========================================
   if (type == UsbControlTransferType::CLASS) {
-    const mojom::UsbInterfaceInfo* interface = nullptr;
-    if (recipient == UsbControlTransferRecipient::ENDPOINT) {
-      interface = device_handle_->FindInterfaceByEndpoint(index & 0xff);
-    } else {
-      // For CLASS requests, we assume index identifies the interface for all
-      // other recipients (INTERFACE, DEVICE, OTHER).
-      interface = FindInterface(config, index & 0xff);
-    }
+    if (recipient == UsbControlTransferRecipient::INTERFACE ||
+        recipient == UsbControlTransferRecipient::ENDPOINT) {
+      const mojom::UsbInterfaceInfo* interface = nullptr;
+      if (recipient == UsbControlTransferRecipient::ENDPOINT) {
+        interface = device_handle_->FindInterfaceByEndpoint(index & 0xff);
+      } else {
+        interface = FindInterface(config, index & 0xff);
+      }
 
-    // Block if the targeted interface is protected.
-    if (interface) {
+      if (!interface) {
+        return BlockAndLog(
+            WebUsbControlTransferPermissionOutcome::kError_InterfaceNotFound);
+      }
+
       auto blocked_class = FindBlockedClass(interface);
       if (blocked_class) {
         LogBlockedControlTransfer(*blocked_class, direction, type);
         return BlockAndLog(WebUsbControlTransferPermissionOutcome::kBlocked);
       }
+
+      return AllowAndLog(WebUsbControlTransferPermissionOutcome::kAllowed);
     }
 
-    // For requests explicitly targeting an INTERFACE or ENDPOINT, the interface
-    // must actually exist in the current configuration.
-    if (recipient == UsbControlTransferRecipient::INTERFACE ||
-        recipient == UsbControlTransferRecipient::ENDPOINT) {
-      return interface ? AllowAndLog(
-                             WebUsbControlTransferPermissionOutcome::kAllowed)
-                       : BlockAndLog(WebUsbControlTransferPermissionOutcome::
-                                         kError_InterfaceNotFound);
-    }
-
-    // For DEVICE and OTHER recipients, if we could not identify the target
-    // interface, we must block it if the device has any protected interfaces.
-    // This prevents bypassing the blocklist by specifying an invalid interface
-    // number (e.g. 0xFF) on a device that ignores the wIndex field.
-    if (!interface && HasProtectedInterface(config)) {
+    // For DEVICE and OTHER recipients, we block the request if the device
+    // has any protected interfaces. This prevents bypassing the blocklist by
+    // specifying a non-protected interface number (or an invalid one) on a
+    // device that ignores the wIndex field.
+    if (HasProtectedInterface(config)) {
       return BlockAndLog(WebUsbControlTransferPermissionOutcome::kBlocked);
     }
 
diff --git a/services/device/usb/mojo/device_impl_unittest.cc b/services/device/usb/mojo/device_impl_unittest.cc
index 7df57bd..cea062d 100644
--- a/services/device/usb/mojo/device_impl_unittest.cc
+++ b/services/device/usb/mojo/device_impl_unittest.cc
@@ -1482,8 +1482,7 @@
   EXPECT_CALL(mock_handle(), Close());
 }
 
-TEST_F(USBDeviceImplTest,
-       ClassControlTransferToDeviceWithProtectedInterfaceBypass) {
+TEST_F(USBDeviceImplTest, ClassControlTransferBlockedIfProtected) {
   // Block interface class 2.
   mojo::Remote<mojom::UsbDevice> device =
       GetMockDeviceProxyWithBlockedInterfaces(base::span_from_ref(uint8_t{2}));
@@ -1496,8 +1495,13 @@
     EXPECT_TRUE(future.Get()->is_success());
   }
 
+  // Interface 0 has class 3 (allowed).
   // Interface 1 has class 2 (blocked).
   AddMockConfig(ConfigBuilder(/*configuration_value=*/1)
+                    .AddInterface(/*interface_number=*/0,
+                                  /*alternate_setting=*/0,
+                                  /*class_code=*/3, /*subclass_code=*/0,
+                                  /*protocol_code=*/0)
                     .AddInterface(/*interface_number=*/1,
                                   /*alternate_setting=*/0,
                                   /*class_code=*/2, /*subclass_code=*/0,
@@ -1512,10 +1516,9 @@
     EXPECT_TRUE(future.Get());
   }
 
+  // Recipient: DEVICE
+  // Case 1: Invalid index (0xFF).
   {
-    // A CLASS request to the DEVICE with index 0xFF (not matching any
-    // interface) should be BLOCKED because the device has a protected interface
-    // (interface 1).
     auto params = mojom::UsbControlTransferParams::New();
     params->type = UsbControlTransferType::CLASS;
     params->recipient = UsbControlTransferRecipient::DEVICE;
@@ -1526,13 +1529,83 @@
     std::vector<uint8_t> fake_data = {1, 2, 3};
     AddMockInboundData(fake_data);
 
-    base::RunLoop loop;
-    device->ControlTransferIn(
-        std::move(params), static_cast<uint32_t>(fake_data.size()), 0,
-        base::BindOnce(&ExpectTransferInAndThen,
-                       mojom::UsbTransferStatus::PERMISSION_DENIED,
-                       std::vector<uint8_t>(), loop.QuitClosure()));
-    loop.Run();
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
+  }
+
+  // Recipient: DEVICE
+  // Case 2: Valid index pointing to non-protected interface (0).
+  {
+    auto params = mojom::UsbControlTransferParams::New();
+    params->type = UsbControlTransferType::CLASS;
+    params->recipient = UsbControlTransferRecipient::DEVICE;
+    params->request = 5;
+    params->value = 6;
+    params->index = 0;  // Valid interface 0 (non-protected)
+
+    std::vector<uint8_t> fake_data = {1, 2, 3};
+    AddMockInboundData(fake_data);
+
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
+  }
+
+  // Recipient: OTHER
+  // Case 1: Invalid index (0xFF).
+  {
+    auto params = mojom::UsbControlTransferParams::New();
+    params->type = UsbControlTransferType::CLASS;
+    params->recipient = UsbControlTransferRecipient::OTHER;
+    params->request = 5;
+    params->value = 6;
+    params->index = 0xFF;  // Does not exist
+
+    std::vector<uint8_t> fake_data = {1, 2, 3};
+    AddMockInboundData(fake_data);
+
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
+  }
+
+  // Recipient: OTHER
+  // Case 2: Valid index pointing to non-protected interface (0).
+  {
+    auto params = mojom::UsbControlTransferParams::New();
+    params->type = UsbControlTransferType::CLASS;
+    params->recipient = UsbControlTransferRecipient::OTHER;
+    params->request = 5;
+    params->value = 6;
+    params->index = 0;  // Valid interface 0 (non-protected)
+
+    std::vector<uint8_t> fake_data = {1, 2, 3};
+    AddMockInboundData(fake_data);
+
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
   }
 
   EXPECT_CALL(mock_handle(), Close());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/device/usb/mojo/device_impl_unittest.cc b/services/device/usb/mojo/device_impl_unittest.cc
index 7df57bd..cea062d 100644
--- a/services/device/usb/mojo/device_impl_unittest.cc
+++ b/services/device/usb/mojo/device_impl_unittest.cc
@@ -1482,8 +1482,7 @@
   EXPECT_CALL(mock_handle(), Close());
 }
 
-TEST_F(USBDeviceImplTest,
-       ClassControlTransferToDeviceWithProtectedInterfaceBypass) {
+TEST_F(USBDeviceImplTest, ClassControlTransferBlockedIfProtected) {
   // Block interface class 2.
   mojo::Remote<mojom::UsbDevice> device =
       GetMockDeviceProxyWithBlockedInterfaces(base::span_from_ref(uint8_t{2}));
@@ -1496,8 +1495,13 @@
     EXPECT_TRUE(future.Get()->is_success());
   }
 
+  // Interface 0 has class 3 (allowed).
   // Interface 1 has class 2 (blocked).
   AddMockConfig(ConfigBuilder(/*configuration_value=*/1)
+                    .AddInterface(/*interface_number=*/0,
+                                  /*alternate_setting=*/0,
+                                  /*class_code=*/3, /*subclass_code=*/0,
+                                  /*protocol_code=*/0)
                     .AddInterface(/*interface_number=*/1,
                                   /*alternate_setting=*/0,
                                   /*class_code=*/2, /*subclass_code=*/0,
@@ -1512,10 +1516,9 @@
     EXPECT_TRUE(future.Get());
   }
 
+  // Recipient: DEVICE
+  // Case 1: Invalid index (0xFF).
   {
-    // A CLASS request to the DEVICE with index 0xFF (not matching any
-    // interface) should be BLOCKED because the device has a protected interface
-    // (interface 1).
     auto params = mojom::UsbControlTransferParams::New();
     params->type = UsbControlTransferType::CLASS;
     params->recipient = UsbControlTransferRecipient::DEVICE;
@@ -1526,13 +1529,83 @@
     std::vector<uint8_t> fake_data = {1, 2, 3};
     AddMockInboundData(fake_data);
 
-    base::RunLoop loop;
-    device->ControlTransferIn(
-        std::move(params), static_cast<uint32_t>(fake_data.size()), 0,
-        base::BindOnce(&ExpectTransferInAndThen,
-                       mojom::UsbTransferStatus::PERMISSION_DENIED,
-                       std::vector<uint8_t>(), loop.QuitClosure()));
-    loop.Run();
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
+  }
+
+  // Recipient: DEVICE
+  // Case 2: Valid index pointing to non-protected interface (0).
+  {
+    auto params = mojom::UsbControlTransferParams::New();
+    params->type = UsbControlTransferType::CLASS;
+    params->recipient = UsbControlTransferRecipient::DEVICE;
+    params->request = 5;
+    params->value = 6;
+    params->index = 0;  // Valid interface 0 (non-protected)
+
+    std::vector<uint8_t> fake_data = {1, 2, 3};
+    AddMockInboundData(fake_data);
+
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
+  }
+
+  // Recipient: OTHER
+  // Case 1: Invalid index (0xFF).
+  {
+    auto params = mojom::UsbControlTransferParams::New();
+    params->type = UsbControlTransferType::CLASS;
+    params->recipient = UsbControlTransferRecipient::OTHER;
+    params->request = 5;
+    params->value = 6;
+    params->index = 0xFF;  // Does not exist
+
+    std::vector<uint8_t> fake_data = {1, 2, 3};
+    AddMockInboundData(fake_data);
+
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
+  }
+
+  // Recipient: OTHER
+  // Case 2: Valid index pointing to non-protected interface (0).
+  {
+    auto params = mojom::UsbControlTransferParams::New();
+    params->type = UsbControlTransferType::CLASS;
+    params->recipient = UsbControlTransferRecipient::OTHER;
+    params->request = 5;
+    params->value = 6;
+    params->index = 0;  // Valid interface 0 (non-protected)
+
+    std::vector<uint8_t> fake_data = {1, 2, 3};
+    AddMockInboundData(fake_data);
+
+    base::test::TestFuture<mojom::UsbTransferStatus, base::span<const uint8_t>>
+        transfer_future;
+    device->ControlTransferIn(std::move(params),
+                              static_cast<uint32_t>(fake_data.size()), 0,
+                              transfer_future.GetCallback());
+    EXPECT_EQ(mojom::UsbTransferStatus::PERMISSION_DENIED,
+              transfer_future.Get<0>());
+    EXPECT_TRUE(transfer_future.Get<1>().empty());
   }
 
   EXPECT_CALL(mock_handle(), Close());
Loading diff…

Original Bug Report

reported by ho...@calif.io

Incomplete fix of crbug **520743499** (fix `872fabf8796f3`, landed 2026-06-15).

Steps to reproduce the problem

There are two repro paths. Path A (unit test) is deterministic and needs no hardware — it is the recommended proof. Path B is the real-world web attack on physical hardware.


Add the test below to services/device/usb/mojo/device_impl_unittest.cc. It mirrors the harness the fix itself uses (the fix only tested index = 0xFF).

Device shape: a composite device whose active configuration has

  • interface 0 = class 0xFF (vendor-specific — NOT on the protected blocklist)
  • interface 1 = class 0x03 (HID — protected/blocklisted)

Call:

// CLASS request, recipient = DEVICE, wIndex low byte = 0 (valid, non-protected interface 0)
bool allowed = device_impl_->HasControlTransferPermission(
    UsbTransferDirection::INBOUND,
    UsbControlTransferType::CLASS,
    UsbControlTransferRecipient::DEVICE,
    /*request=*/0x01,        // e.g. HID GET_REPORT
    /*value=*/0x0100,
    /*index=*/0x0000);       // resolves to interface 0 (not protected)

Expected (correct) result: false (blocked) — the device has a protected interface and the request to recipient DEVICE cannot be safely routed by wIndex.

Actual result on 151.0.7896.0: true (allowed). FindInterface(config, 0) returns the non-protected interface 0, FindBlockedClass returns nullopt, the explicit INTERFACE/ENDPOINT branch is skipped, and !interface is false so the new HasProtectedInterface guard is skipped — control falls through to return AllowAndLog(kAllowed).

Contrast: the same call with index = 0x00FF (invalid interface) correctly returns false — that is the only case the 872fabf8 fix and its tests cover.


Path B — Live web reproduction (physical composite device)

Preconditions:

  1. A composite USB device whose active configuration exposes BOTH a non-protected interface (e.g. vendor/CDC, interface 0) AND a protected-class interface (e.g. HID, interface 1), and whose firmware services the class request on the control endpoint without strictly validating that wIndex names interface 1 (common — e.g. WinUSB forces wIndex to the interface number; many HID stacks act on SET_REPORT/GET_REPORT regardless of the wIndex interface field).
  2. The user grants the origin access to this device once (normal WebUSB grant).

Page script:

const dev = (await navigator.usb.getDevices())[0]
         || await navigator.usb.requestDevice({ filters: [] });
await dev.open();
await dev.selectConfiguration(1);
// NOTE: we do NOT claimInterface() the protected interface — a DEVICE-recipient
// control transfer requires no interface claim in Blink.

// CLASS request to recipient 'device', wIndex naming the NON-protected interface 0.
const res = await dev.controlTransferIn({
  requestType: 'class',
  recipient:   'device',
  request:     0x01,      // e.g. HID GET_REPORT
  value:       0x0100,
  index:       0x0000     // low byte = interface 0 (valid, non-protected)
}, 64);
console.log('reached device, status =', res.status);

Expected (correct): the transfer is blocked by the protected-class policy (it would reach a HID interface). Actual on 151.0.7896.0: the transfer is delivered to the device; firmware that ignores/misinterprets wIndex executes it on the protected HID interface.

A control-OUT variant (controlTransferOut, e.g. HID SET_REPORT, request 0x09) lets the page write to the protected interface the same way.

Problem Description

WebUSB enforces a “protected interface class” blocklist so that a web origin which the user has granted access to a USB device still cannot send control transfers that reach sensitive interface classes (HID, mass-storage, smart-card, audio/video). The enforcement point is DeviceImpl::HasControlTransferPermission in the device service.

Commit 872fabf8796f3 (“usb: Fix protected class bypass via crafted wIndex in CLASS requests”, Bug 520743499, landed 2026-06-15) fixed one bypass: a CLASS request to recipient DEVICE/OTHER with an invalid wIndex (e.g. 0xFF) on a device that ignores wIndex. The fix added if (!interface && HasProtectedInterface(config)) block;.

That guard is gated on !interface — it only fires when wIndex resolves to no interface. But the fix’s own threat model is a device that ignores wIndex, which ignores a valid wIndex just the same. So a CLASS request to recipient DEVICE with wIndex naming a valid non-protected interface (e.g. 0) on a composite device that also exposes a protected interface skips the guard and is allowed, reaching the protected interface. The protections are enabled by default; DEVICE/OTHER recipients require no interface claim. This is an incomplete fix with a distinct trigger (valid wIndex vs the fixed 0xFF case) and no test coverage — a fresh, non-duplicate bypass of the same security boundary.

Summary

Incomplete fix of crbug 520743499 (fix 872fabf8796f3, landed 2026-06-15).

Additional Data

Category: Security
Chrome Channel: Canary
Regression: N/A \

View on issue tracker