Repository: electron/electron
Pull request: fix: validate USB device selection against filtered device list

#### Description of Change

Previously, `UsbChooserController::OnDeviceChosen` looked up the chosen `device_id` via `chooser_context_->GetDeviceInfo()`, which searches all known USB devices on the system rather than the filtered list shown to the `select-usb-device` handler. This meant a device excluded by the renderer's filters or `exclusion_filters` could still be granted permission if the handler returned its GUID.

This aligns the USB chooser with the Serial and HID chooser implementations, which correctly validate against their filtered lists.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [ ] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: none

Changed files:
filename: shell/browser/usb/usb_chooser_controller.cc
patch: @@ -68,6 +68,7 @@ gin::WeakCell<api::Session>* UsbChooserController::GetSession() {
 void UsbChooserController::OnDeviceAdded(
     const device::mojom::UsbDeviceInfo& device_info) {
   if (DisplayDevice(device_info)) {
+    devices_.push_back(device_info.Clone());
     gin::WeakCell<api::Session>* session = GetSession();
     if (session && session->Get()) {
       session->Get()->Emit("usb-device-added", device_info.Clone(),
@@ -78,6 +79,9 @@ void UsbChooserController::OnDeviceAdded(
 
 void UsbChooserController::OnDeviceRemoved(
     const device::mojom::UsbDeviceInfo& device_info) {
+  std::erase_if(devices_, [&device_info](const auto& device) {
+    return device->guid == device_info.guid;
+  });
   gin::WeakCell<api::Session>* session = GetSession();
   if (session && session->Get()) {
     session->Get()->Emit("usb-device-removed", device_info.Clone(),
@@ -90,9 +94,11 @@ void UsbChooserController::OnDeviceChosen(gin::Arguments* args) {
   if (!args->GetNext(&device_id) || device_id.empty()) {
     RunCallback(/*device_info=*/nullptr);
   } else {
-    auto* device_info = chooser_context_->GetDeviceInfo(device_id);
-    if (device_info) {
-      RunCallback(device_info->Clone());
+    const auto it = std::ranges::find_if(
+        devices_,
+        [&device_id](const auto& device) { return device->guid == device_id; });
+    if (it != devices_.end()) {
+      RunCallback((*it)->Clone());
     } else {
       util::EmitWarning(
           base::StrCat({"The device id ", device_id, " was not found."}),
@@ -127,6 +133,11 @@ void UsbChooserController::GotUsbDeviceList(
       return !DisplayDevice(*device_info);
     });
 
+    devices_.clear();
+    for (const auto& device : devices) {
+      devices_.push_back(device->Clone());
+    }
+
     v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
                                         .Set("deviceList", devices)
                                         .Set("frame", rfh)

filename: shell/browser/usb/usb_chooser_controller.h
patch: @@ -5,6 +5,7 @@
 #ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_
 #define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_
 
+#include <string>
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
@@ -75,6 +76,9 @@ class UsbChooserController final : private UsbChooserContext::DeviceObserver,
 
   base::WeakPtr<ElectronUsbDelegate> usb_delegate_;
 
+  // Filtered list of devices that passed DisplayDevice()
+  std::vector<device::mojom::UsbDeviceInfoPtr> devices_;
+
   content::GlobalRenderFrameHostId render_frame_host_id_;
 
   base::WeakPtrFactory<UsbChooserController> weak_factory_{this};