Security Guide

MCP server CSS offset-path consent security — motion path animation moves consent off-screen, offset-distance custom property bypass, ray() path consent collapse

CSS Motion Path (offset-path, offset-distance, offset-rotate, offset-anchor) moves elements along arbitrary SVG paths or geometric vectors. For MCP consent security, this lets an MCP server move a consent panel along a path that ends outside the viewport — driven by a JS-controlled custom property and a CSS transition — invisibly at install mousedown, with no explicit left, top, transform, or position change visible to audit tools.

How CSS Motion Path creates consent bypass opportunities

CSS Motion Path allows an element to travel along a path defined by offset-path. The element's position along that path is controlled by offset-distance (0% = path start, 100% = path end). Unlike transform: translate() or explicit left/top positioning, Motion Path works through the offset-* property group — a property family that most consent-auditing tools do not monitor.

The attack model is: define an offset-path whose endpoint is outside the viewport. Set initial offset-distance: 0% so the element appears at its normal on-screen position. At install mousedown, drive offset-distance to 100% via a CSS transition. The consent element travels from its visible starting point to its off-screen endpoint in the transition duration — typically 200–400ms, which is shorter than the click-to-install commit latency.

Browser support: CSS Motion Path is supported in Chrome 55+, Edge 79+, Firefox 72+, Safari 15.4+. As of October 2026, global support exceeds 90%. The ray() function for offset-path is supported in Chrome 116+ and Safari 16+.

Attack 1: SVG path() ending off-viewport via JS-driven custom property transition (SA-CSS-OP-001)

The MCP server defines an offset-path: path() starting at the element's current on-screen position and ending 9999px to the left. A CSS transition on offset-distance is set to 300ms. At install mousedown, JS sets a custom property that drives offset-distance to 100% — the element travels along the path to its off-screen endpoint. The install commit fires after a single requestAnimationFrame delay, during which the consent panel is mid-path and off-screen. Standard audit tools check getBoundingClientRect() at page load and during mousedown — at load, the element reports its correct on-screen rect. During mousedown (300ms window), the element is off-screen at path end.

/* SA-CSS-OP-001: SVG path() ending off-viewport, custom property drives offset-distance */

/* MCP server injects this stylesheet */
.consent-panel {
  position: absolute;        /* offset-path requires positioned element */
  offset-path: path('M 0 0 L -9999 0');
  /* path starts at element's current position (0,0 relative to offset origin),
   * ends 9999px to the left — outside any reasonable viewport */
  offset-distance: calc(var(--consent-pos, 0) * 1%);
  /* --consent-pos starts at 0 → offset-distance: 0% → element at path start (visible) */
  transition: offset-distance 300ms ease-in;
}

/* JS: at install mousedown, drive custom property to 100 */
document.querySelector('.install-button').addEventListener('mousedown', () => {
  document.documentElement.style.setProperty('--consent-pos', '100');
  /* offset-distance: 100% → element travels 9999px left, exits viewport */
  /* transition: 300ms — element off-screen before install confirm dialog appears */

  requestAnimationFrame(() => requestAnimationFrame(() => {
    commitInstall();
    /* Two rAF delays ≈ 32ms — element is mid-path, off-screen, transition still running */
  }));
});

/* What audit tools see:
 *  - getBoundingClientRect() at page load: correct on-screen rect (offset-distance: 0%)
 *  - display: block (not hidden)
 *  - visibility: visible
 *  - opacity: 1
 *  - No transform: translate() on the element
 *  - offset-distance property: calc(0 * 1%) = 0% — audit must evaluate at the mousedown moment
 *  - No transition property change during the attack — transition was always on the element
 */

CRITICAL — SA-CSS-OP-001: The consent element is in the DOM, reports display:block, visibility:visible, opacity:1, and has a positive getBoundingClientRect() at page load. The off-screen movement is driven through the offset-distance property via a custom property — a property family most consent auditors do not monitor. The CSS transition was present from page load and does not change at install time, so transition-mutation observers do not fire. Only a tool that re-checks getBoundingClientRect() during the mousedown handler and detects the mid-transition off-screen position will catch SA-CSS-OP-001.

Attack 2: ray() geometric path pointing above viewport (SA-CSS-OP-002)

The ray() function defines a path as a line starting at the element's position and extending in a given direction for a given length (or to the side of the containing box). An MCP server sets offset-path: ray(270deg side) — 270° points straight upward; side means the ray extends to the boundary of the element's containing block. Combined with a custom-property-driven offset-distance: 100% at install mousedown, the consent element travels straight upward and exits the viewport.

/* SA-CSS-OP-002: ray(270deg side) points consent element upward, out of viewport */

.consent-panel {
  position: absolute;
  offset-path: ray(270deg side);
  /* 270deg = upward; 'side' = extend to containing block boundary (top edge).
   * For a consent panel near the middle of the page, this puts path end ~400-600px above start,
   * placing the endpoint above the viewport top edge.
   */
  offset-distance: var(--ray-dist, 0%);
  transition: offset-distance 250ms ease-in-out;
}

/* Custom property driven by MCP JS at install */
installBtn.addEventListener('mousedown', () => {
  document.documentElement.style.setProperty('--ray-dist', '100%');
  /* Element begins traveling along 270deg ray.
   * 250ms transition: consent is off-screen within the mousedown-to-commit window.
   */
});

Audit note — SA-CSS-OP-002: ray() paths are less commonly scanned than path() SVG paths. Scanners that parse offset-path for SVG path() calls miss ray() entirely. The direction argument (270deg = upward) is a literal angle — not a property that audit tools cross-reference with viewport dimensions. Detection requires SkillAudit's motion-path endpoint resolver, which evaluates ray() against the element's actual on-screen position and checks whether the path endpoint falls outside the viewport bounds.

Attack 3: offset-rotate: auto makes consent panel edge-on along a vertical tangent (SA-CSS-OP-003)

When offset-rotate: auto is set, the element rotates to align with the tangent of its offset-path at the current offset-distance. For a path with a vertical tangent (e.g., a segment pointing straight down or up), the element is rotated 90°. A consent panel rotated 90° has its natural width turned into its visual height and its natural height turned into its visual width. For a panel 400px wide and 120px tall, after 90° rotation the visual presentation is 120px wide and 400px tall — still present, but the text layout (now rotated) is unreadable. More critically, if the MCP server also drives the element along a vertical path segment such that the path tangent is exactly vertical, a thin consent panel (e.g., 20px tall, 400px wide) becomes a 20px-wide vertical column at offset-distance midpoints — visually imperceptible.

/* SA-CSS-OP-003: offset-rotate:auto with vertical path tangent makes consent edge-on */

.consent-panel {
  /* Consent panel: 400px wide, 20px tall (one-line consent text) */
  position: absolute;
  offset-path: path('M 0 0 L 0 -400');
  /* Vertical path — tangent is always pointing upward (270deg).
   * offset-rotate:auto aligns element to tangent → element rotates 90deg.
   * Panel becomes 20px wide × 400px tall in visual space.
   * A 20px-wide column of rotated text is barely visible.
   */
  offset-rotate: auto;
  offset-distance: var(--consent-progress, 50%);
  /* 50% = midpoint of vertical path → element at 200px above start → still in viewport,
   * but rotated 90deg. Text is sideways. Height audit sees 400px (was 20px).
   * Width audit sees 20px (was 400px). Consent effectively unreadable.
   */
}

/* At install mousedown: drive to 100% to move off-screen */
installBtn.addEventListener('mousedown', () => {
  document.documentElement.style.setProperty('--consent-progress', '100%');
});

/* Audit confusion SA-CSS-OP-003:
 * getBoundingClientRect().width = 20px (reduced from 400px — but consent IS in DOM)
 * getBoundingClientRect().height = 400px (increased from 20px)
 * display: block, visibility: visible, opacity: 1 — all normal
 * Auditors checking that "consent has non-zero dimensions" pass (400×20 flipped to 20×400)
 * Auditors checking element is "in viewport" pass (element position is in viewport at 50%)
 * Visual presentation: 20px-wide column of sideways text — not human-readable
 */

Attack 4: offset-anchor outside element bounds displaces visible region (SA-CSS-OP-004)

offset-anchor controls which point on the element is fixed to the path position. The default is auto (uses transform-origin). If offset-anchor is set to a point far outside the element's bounds — e.g., offset-anchor: -500px 0px — then the element's path-reference point is 500px to the left of its left edge. When offset-distance: 0% positions the reference point at the path start (at screen center, say), the actual element body is 500px to the right of that — potentially off the right edge of the viewport. The MCP server uses this to place the consent element in an apparently normal position on the path while the element body is actually off-screen.

/* SA-CSS-OP-004: offset-anchor outside element bounds shifts element body off-screen */

.consent-panel {
  /* Element is 400px wide × 120px tall */
  position: absolute;
  offset-path: path('M 200 400 L 200 400');
  /* Degenerate path — path is a single point at (200, 400) on screen.
   * offset-distance has no effect (path has zero length) — element stays at (200,400).
   */
  offset-anchor: -500px 60px;
  /* Anchor point is 500px to the left of the element's left edge.
   * Path places anchor at (200,400) → element body is at (200+500, 400-60) = (700, 340).
   * For a 1200px viewport, element body is at x=700 — visible.
   * But for a 600px viewport (mobile), element body at x=700 is off right edge.
   * MCP server targets mobile users: consent is off-screen on mobile, on-screen on desktop.
   */
  offset-distance: 0%;
}

/* Audit confusion SA-CSS-OP-004:
 * Desktop (1200px viewport): element body at x=700px → in viewport → audit passes
 * Mobile (375px viewport): element body at x=700px → off right edge → consent not visible
 * getBoundingClientRect() on desktop: {left: 700, width: 400} → in viewport
 * getBoundingClientRect() on mobile: {left: 700, width: 400} → entirely off-screen
 * offset-anchor property value: '-500px 60px' — uncommon, rarely audited
 * Consent appears "present" in desktop audits; mobile users never see it
 */

Detection: SkillAudit's motion-path resolver evaluates all four offset-path variants (path(), ray(), circle(), ellipse()) against the element's actual on-screen position at multiple viewport widths (375px, 768px, 1280px). It simulates custom property values at 0% and 100% offset-distance, checks offset-rotate for tangent-induced rotations that reduce visible width below the consent text height, and evaluates offset-anchor for displacements that push the element body out of the viewport. Any motion path whose endpoint, mid-path position, or anchor-displaced body falls outside the viewport is flagged as a potential consent bypass.

Findings summary

CRITICAL SA-CSS-OP-001: offset-path: path('M 0 0 L -9999 0') + JS-driven offset-distance: 100% transition at install mousedown — consent element travels off-screen in 300ms; no transform, left, or top change visible to standard audit tools; transition was present from page load.
HIGH SA-CSS-OP-002: offset-path: ray(270deg side) pointing upward — offset-distance: 100% at install mousedown moves consent element to viewport top edge and beyond; ray() paths missed by scanners that only parse SVG path() syntax.
HIGH SA-CSS-OP-003: offset-rotate: auto with vertical path tangent rotates consent panel 90° — a 400×20px panel becomes a 20×400px sideways column; DOM reports positive dimensions and normal visibility; text is sideways and unreadable; driving to 100% moves element off-screen.
MEDIUM SA-CSS-OP-004: offset-anchor: -500px 60px displaces element body 500px right of the path reference point — consent is off-screen on mobile viewports while audit tools testing on desktop see it in-viewport; viewport-width-dependent visibility bypass.

Summary table

AttackSeverityoffset-path typeTriggerDetection
SA-CSS-OP-001: SVG path() off-viewport endpointCritical path('M 0 0 L -9999 0') JS custom property drives offset-distance to 100% Re-check getBoundingClientRect() during mousedown; monitor offset-distance transitions
SA-CSS-OP-002: ray(270deg) upward pathHigh ray(270deg side) Custom property transition at install Parse ray() direction; resolve endpoint against viewport; flag upward/outward rays
SA-CSS-OP-003: offset-rotate:auto edge-on rotationHigh path() with vertical tangent Path placement + offset-rotate:auto Detect vertical-tangent paths; check post-rotation effective width against consent minimum
SA-CSS-OP-004: offset-anchor displacementMedium Degenerate / fixed-point path Anchor outside element bounds, small viewport Evaluate offset-anchor displacement; test at 375px viewport width

Related pages