MCP Server Security · WebUSB API · USB Device Enumeration · Hardware Security Key · Physical Device Attack · Chrome Only
MCP server WebUSB security
The WebUSB API (navigator.usb.requestDevice()) provides JavaScript bidirectional communication to USB-connected physical hardware after a one-time browser permission dialog. MCP tool output can use a "device setup" or "firmware update" pretext to acquire a handle to connected USB devices — including hardware security keys (YubiKey, Titan) where the FIDO2 interface is accessible via USB control transfers, Arduino and embedded controllers with writable firmware, and Android devices in ADB mode. Available in Chrome desktop and Electron-based MCP clients (Claude Desktop, Cursor, Windsurf). No Permissions-Policy directive blocks it.
API surface: requestDevice() and device communication
// WebUSB — Chrome desktop, Edge desktop, Electron only
// Shows a device picker dialog — user must explicitly select and click Connect
// Step 1: Request a device — filter targets YubiKey / Titan security keys
const device = await navigator.usb.requestDevice({
filters: [
{ vendorId: 0x1050 }, // Yubico
{ vendorId: 0x18d1 }, // Google (Titan Key)
{ vendorId: 0x096e }, // Feitian
// Empty filter {} matches ANY connected USB device
]
});
// Step 2: Open the device and claim its interface
await device.open();
await device.selectConfiguration(1); // Select USB configuration 1
// Claim the FIDO HID interface (interface 0 on most security keys)
await device.claimInterface(0);
// Step 3: Send a CTAP2/FIDO2 command via control transfer
// This sends a FIDO2 "authenticatorGetInfo" command to the key
const ctapCmd = new Uint8Array([
0x04, // CTAP2 command byte: authenticatorGetInfo
]);
await device.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x09, // HID SET_REPORT
value: 0x0200, // Report type: Output
index: 0 // Interface 0
}, ctapCmd);
// Step 4: Read the key's response (capability information)
const result = await device.controlTransferIn({
requestType: 'class',
recipient: 'interface',
request: 0x01, // HID GET_REPORT
value: 0x0100, // Report type: Input
index: 0
}, 64); // 64 bytes — standard FIDO2 response packet size
const responseBytes = new Uint8Array(result.data.buffer);
// responseBytes contains authenticator capabilities, AAGUID (device model identifier),
// supported algorithms, extensions — and may include counter value information
Hardware security key access is the highest-impact attack path: FIDO2 security keys (YubiKey, Titan, Feitian) expose their full USB HID interface via WebUSB. While private key material cannot be extracted from a compliant key, the CTAP2 protocol includes management commands (clientPIN, reset, credential management) that can manipulate the key's stored credentials, counter values, and PIN state — operations that are devastating in a targeted attack. A reset() command wipes all resident credentials from the key. A credential management deleteCredential() removes a specific passkey. These operations complete in under 100ms.
Attack paths by device type
| Device type | USB VendorID | Attack impact | Requires user interaction? |
|---|---|---|---|
| YubiKey (Yubico) | 0x1050 | FIDO2 credential management: enumerate resident keys, delete credentials, read AAGUID and serial number, PIN management | Device picker (one click) |
| Google Titan Key | 0x18d1 | Same FIDO2 CTAP2 surface as YubiKey; Google's attestation AAGUID readable; reset() wipes all credentials | Device picker (one click) |
| Arduino / ESP32 | 0x2341 / 0x303a | Bidirectional serial control of hardware; firmware flash via DFU protocol; I/O pin control; sensor data read | Device picker (one click) |
| Android (ADB mode) | 0x18d1 (ADB) | ADB protocol over bulk transfers: adb shell, file push/pull, package install — full Android device control | Device picker + Android USB debugging authorization |
| USB mass storage | Various | File system read/write via bulk-only transport; bypasses OS file picker restrictions | Device picker (one click) |
Enumeration without permission: navigator.usb.getDevices()
Previously granted device permissions persist across page loads for the origin. navigator.usb.getDevices() returns all devices the user has previously granted access to for this origin — without any new dialog. This means an MCP server that has previously obtained USB permission (via a legitimate-looking "setup" flow) can silently re-access those devices in subsequent tool outputs:
// Silent re-access of previously granted devices — no dialog
const previouslyGranted = await navigator.usb.getDevices();
// Returns array of USBDevice objects for all devices the user granted this origin before
for (const device of previouslyGranted) {
console.log(`${device.manufacturerName} ${device.productName} (${device.vendorId}:${device.productId})`);
// Can immediately open and communicate without showing any UI
}
Browser support
| Browser / Client | WebUSB? | Notes |
|---|---|---|
| Chrome 61+ (desktop) | Yes | Full API including getDevices(); permission persists per origin |
| Edge 79+ (desktop) | Yes | Inherited from Chromium; same behavior |
| Claude Desktop, Cursor, Windsurf (Electron) | Yes | Electron exposes full WebUSB; no restriction; highest risk for hardware key attacks |
| Chrome Android | Yes (USB OTG devices) | Works with USB OTG-connected devices on Android; less common attack surface |
| Firefox | No | Mozilla declined to implement WebUSB citing security concerns; not available |
| Safari | No | WebKit has not implemented WebUSB |
SkillAudit findings
navigator.usb.requestDevice() with filters targeting hardware security key vendor IDs (0x1050 Yubico, 0x18d1 Google, 0x096e Feitian) — FIDO2 authenticator access enabling credential management attacks
device.controlTransferOut() with CTAP2 command bytes targeting a claimed FIDO2 interface — sending authenticator management commands to hardware security key
navigator.usb.getDevices() to silently enumerate and re-access previously granted USB devices without showing any permission dialog
navigator.usb.requestDevice({filters:[{}]}) with an empty filter matching any USB device — broad device access intended to enumerate all connected hardware
navigator.usb.requestDevice() for any purpose without disclosing in documentation what USB devices are accessed and why
Related: WebHID Security · Web Serial API Security · File System Access API Security · Run a SkillAudit →