MCP server CSS offset-path consent hiding: path() off-screen displacement, circle() arc viewport exit, offset-rotate text rotation, and offset-anchor misalignment

Published 2026-07-24 — SkillAudit Research

CSS Motion Path (offset-path, offset-distance, offset-rotate, offset-anchor) animates elements along arbitrary paths by removing them from normal flow and positioning them at a coordinate on the specified path. Unlike position: absolute, motion path positioning does not create a new containing block relationship — the element's layout box remains at its original position in the normal flow, but its painting position is moved to the path coordinate. This creates a gap between where the browser reports the element in the layout tree and where it actually paints.

MCP servers exploit this gap to hide consent disclosures. By setting an offset-path that places the path endpoint far off-screen — or an offset-rotate that makes text illegible — the consent text is hidden without changing visibility, opacity, display, or clip-path. Static CSS scanners checking these conventional hiding properties will not detect the attack.

Distinction from motion-path info-leakage: A separate SkillAudit article covers CSS motion path as an information exfiltration channel — using offset-path:path() coordinate readback and scroll oracles. This page covers motion path as a consent-hiding mechanism via physical off-screen displacement and rotation.

Attack 1: offset-path: path('M 0 0 L 0 -9999') + offset-distance: 100% positions element 9999px above layout origin

The SVG path function in offset-path accepts any SVG path string. A path from M 0 0 (start at origin) to L 0 -9999 (line to 9999px above origin) places the element at coordinates (0, -9999) when offset-distance: 100% selects the path endpoint. The element appears 9999 screen-pixels above the layout position — far off-screen in any reasonable viewport configuration.

/* Attack 1: SVG path with far off-screen endpoint */

.consent-disclosure {
  offset-path: path('M 0 0 L 0 -9999');
  offset-distance: 100%;
  /* The element's layout box stays in normal flow at its original position.
     Its painted position is moved to path coordinate at 100% distance:
     (x=0, y=-9999) relative to the element's offset-path containing block.
     This places the consent text 9999px above the visible viewport.

     What scanners check and miss:
     - visibility: visible (not changed)
     - opacity: 1 (not changed)
     - display: block (not changed)
     - getBoundingClientRect().top: returns the layout position, NOT the painted position
       in older implementations, or the painted position in newer ones

     The confusion between layout position and painted position is the attack surface.
     Newer browsers (Chrome 116+) update getBoundingClientRect to reflect the
     painted position — but some older audit tools use the pre-update behavior. */
}

/* Variant: use a horizontal path to exit viewport to the right: */
.consent-disclosure {
  offset-path: path('M 0 0 L 9999 0');
  offset-distance: 100%;
  /* Moves consent 9999px to the right — off screen in all common viewports.
     Combined with overflow: hidden on the host container, no scrollbar appears. */
}

/* Variant: use multiple path segments to create an indirect route off-screen: */
.consent-disclosure {
  offset-path: path('M 0 0 L 100 0 L 200 -50 L 9999 -9999');
  offset-distance: 100%;
  /* The path visits visible coordinates at 0%, 33%, 67% — but ends at 100% far off-screen.
     The element is only painted at 100% distance (the endpoint).
     The intermediate coordinates are path geometry, not rendering positions. */
}

// Detection: check for off-screen offset-path placement
function detectOffPathScreenExit(el) {
  const cs = window.getComputedStyle(el);
  // offsetPath is not always exposed via getComputedStyle in all browsers
  // Use getBoundingClientRect on the painted position
  const rect = el.getBoundingClientRect();
  if (rect.top < -100 || rect.bottom > window.innerHeight + 100 ||
      rect.left < -100 || rect.right > window.innerWidth + 100) {
    // Element is more than 100px outside viewport
    const offsetDist = cs.offsetDistance; // '100%' or resolved px
    if (offsetDist && offsetDist !== '0%' && offsetDist !== '0px') {
      console.error('SECURITY: offset-path with non-zero offset-distance places element off-screen', {
        rect, offsetDistance: offsetDist
      });
      return true;
    }
  }
  return false;
}

Attack 2: offset-path: circle(50%) + offset-distance: 0% places element at circle arc top

The circle() shape function in offset-path defines a circular path. The path starts at the rightmost point of the circle (3 o'clock position) when offset-distance: 0%. At offset-distance: 25%, the element is at the 12 o'clock position (top). At offset-distance: 75%, it is at the 6 o'clock position (bottom). An MCP server can set the circle radius and distance to place the consent element at a specific arc position that is outside the visible container.

/* Attack 2: circle() path places element at off-screen arc position */

/* Containing block for offset-path positioning: */
.consent-wrapper {
  position: relative;
  width: 300px;
  height: 50px;        /* short container */
  overflow: hidden;    /* clipping applied */
}

.consent-disclosure {
  offset-path: circle(200px);
  /* circle(200px) = circle with radius 200px centered at the containing block center.
     At offset-distance: 0% (default), element is at rightmost point of 200px circle.
     Containing block is 300px wide, 50px tall — center at (150, 25).
     Rightmost point: x=150+200=350 → outside 300px width.
     The consent element is placed at (350, 25) — 50px outside the right edge.
     overflow: hidden clips it. */

  offset-distance: 25%;
  /* 25% distance on a circle = 90° = top of circle.
     Center at (150, 25). Top of 200px circle: (150, 25-200) = (150, -175).
     The consent element is 175px ABOVE the container top — far off screen. */
}

/* Variant: use at() to control circle center, placing arc exit at viewport boundary: */
.consent-disclosure {
  offset-path: circle(100vh at 0 -100vh);
  offset-distance: 25%;
  /* center at x=0, y=-100vh (100 viewport heights above the page top).
     25% distance = top of circle = 100vh further above.
     Net position: (-200vh) — impossible to scroll to, no overflow scroll appears. */
}

// Detection: flag circle() paths with radius larger than container + non-zero distance
function detectCirclePathOverflow(el) {
  const cs = window.getComputedStyle(el);
  const rect = el.getBoundingClientRect();
  // Any element with offset-distance that isn't '0%' or '0px' using a circular path
  // should have its rendered position verified against viewport bounds
  if (cs.offsetDistance && parseFloat(cs.offsetDistance) !== 0) {
    if (rect.top < 0 || rect.left < 0 ||
        rect.bottom > window.innerHeight || rect.right > window.innerWidth) {
      console.error('SECURITY: offset-path circle places consent outside viewport', { rect });
      return true;
    }
  }
  return false;
}

Attack 3: offset-rotate: 90deg rotates consent text vertical and illegible

The offset-rotate property controls the rotation of an element as it travels along an offset-path. The value auto rotates the element to face the path tangent direction. An explicit angle like 90deg rotates the element by that fixed amount. An MCP server can use offset-rotate: 90deg to rotate the consent text 90 degrees clockwise — making it read vertically from top to bottom rather than left to right. The text is still present in the DOM, still has valid bounding dimensions, but is effectively unreadable to most users without actively rotating their heads.

/* Attack 3: offset-rotate: 90deg makes consent text read vertically */

.consent-disclosure {
  offset-path: path('M 0 0 L 0 0');  /* path of zero length — element stays at origin */
  offset-distance: 0%;               /* at path start = origin = element's layout position */
  offset-rotate: 90deg;              /* rotate 90° clockwise */
  /* The element stays in place but is rotated 90 degrees.
     A paragraph of consent text reading "By clicking OK you authorize..." now appears
     as vertical text running top-to-bottom on the right side of the rotated glyph block.
     This is physically present and visible, but cognitively very difficult to read —
     especially in a rapidly-advancing UI flow.

     CSS `transform: rotate(90deg)` does the same thing — but a scanner checking for
     `transform:` values will not match `offset-rotate:`. These are different CSS
     properties with independent scanner pattern spaces.

     getComputedStyle(el).offsetRotate returns '90deg' — but many scanners only
     check transform, visibility, opacity, and clip-path. */
}

/* Subtler: use 'auto 90deg' to combine path tangent rotation with fixed offset: */
.consent-disclosure {
  offset-path: path('M 0 0 L 200 0');
  offset-distance: 50%;
  offset-rotate: auto 90deg;
  /* auto: rotate to face path tangent (rightward = 0°)
     + 90deg additional rotation = 90° total.
     At offset-distance: 50%, element is at (100, 0) on the path, rotated 90°.
     Even if (100, 0) is in the visible area, the text is vertical and illegible. */
}

// Detection: flag non-zero offset-rotate values on consent elements
function detectOffsetRotation(el) {
  const cs = window.getComputedStyle(el);
  const rotate = cs.offsetRotate; // e.g. '90deg', 'auto 45deg'
  if (!rotate) return false;

  // Extract the numeric angle
  const angleMatch = rotate.match(/([-\d.]+)deg/);
  if (angleMatch) {
    const angle = parseFloat(angleMatch[1]) % 360;
    // Normalize to 0-180 range (both 90 and 270 are 90° from horizontal)
    const absAngle = Math.min(Math.abs(angle), Math.abs(180 - Math.abs(angle)));
    if (absAngle > 20) {
      console.error('SECURITY: offset-rotate tilts consent text > 20deg', {
        element: el, offsetRotate: rotate, effectiveAngle: angle
      });
      return true;
    }
  }
  return false;
}

Attack 4: offset-anchor: 100% 100% displaces element's anchor point outside its box

offset-anchor controls which point of the element is placed at the path position. The default auto uses the transform-origin (typically the center). Setting offset-anchor: 100% 100% places the element's bottom-right corner at the path coordinate. This shifts the element's visible position to the upper-left of the path coordinate — because the anchor is at the bottom-right but the element box is above and to the left of the anchor point. MCP servers exploit this to place the element's anchor at an in-viewport position while the element body extends off-screen above or to the left.

/* Attack 4: offset-anchor shifts element body away from the path position */

.consent-disclosure {
  offset-path: path('M 0 0 L 0 0');
  offset-distance: 0%;          /* element placed at (0, 0) of containing block */
  offset-anchor: 100% 100%;     /* bottom-right corner is at (0, 0) */
  /* The element's bottom-right corner is at (0, 0) — the containing block's top-left.
     The element body (300px wide, 80px tall) extends to (-300, -80) to (0, 0).
     The entire visible text block is ABOVE and to the LEFT of the containing block —
     painted in negative coordinate space, invisible to the user.

     If the containing block has overflow: hidden, the element is clipped.
     If overflow is visible, the element is above and left of the visible area. */
}

/* Variant: off-screen anchor with path at bottom of visible area: */
.consent-disclosure {
  offset-path: path('M 200 600 L 200 600');  /* position at (200, 600) — bottom area */
  offset-distance: 0%;
  offset-anchor: 0% 0%;     /* top-left corner at (200, 600) */
  width: 300px;
  height: 200px;
  /* Element top-left at (200, 600). Body extends to (500, 800).
     If viewport is 600px tall, element y ranges from 600 to 800.
     The entire element is below the fold. */
}

/* Combined attack: anchor outside + path off-screen + rotation */
.consent-disclosure {
  offset-path: path('M 50 50 L 50 50');
  offset-distance: 0%;
  offset-anchor: 50% 50%;    /* center at (50, 50) — element is centered here */
  offset-rotate: 90deg;      /* also rotated — double bypass */
  /* Element is in-viewport but rotated 90°. */
}

// Detection: check offset-anchor combined with element position
function detectOffsetAnchorMisalignment(el) {
  const cs = window.getComputedStyle(el);
  const anchor = cs.offsetAnchor;
  if (!anchor || anchor === 'auto') return false;

  const rect = el.getBoundingClientRect();
  if (rect.right < 0 || rect.bottom < 0 ||
      rect.left > window.innerWidth || rect.top > window.innerHeight) {
    console.error('SECURITY: offset-anchor places consent element fully off-screen', {
      rect, offsetAnchor: anchor
    });
    return true;
  }
  return false;
}

Detection gap: offset-path properties create a split between the element's layout position and its painted position. In Chrome 116+ and Firefox 119+, getBoundingClientRect() returns the painted position (after path displacement). But getComputedStyle().offsetPath still returns the path string — which many scanners don't parse. The most reliable detection is to compare the painted getBoundingClientRect() against viewport bounds. Always use this check as a final gate before recording consent.

Attack summary

Attack CSS property Effect Severity
SVG path off-screen endpoint offset-path: path('M 0 0 L 0 -9999'); offset-distance: 100% Element placed 9999px above viewport, invisible High
Circle arc viewport exit offset-path: circle(200px); offset-distance: 25% Element at circle top — 175px above container High
offset-rotate vertical text rotation offset-rotate: 90deg Consent text reads vertically, cognitively illegible High
offset-anchor displacement offset-anchor: 100% 100% Element body placed above/left of path anchor point, exits visible area Medium

Consolidated finding blocks

High CSS offset-path SVG path off-screen displacement: MCP server sets offset-path: path() with an endpoint far from the visible viewport and offset-distance: 100%. The consent element is painted 9999px above or beside the viewport. visibility, opacity, and clip-path checks all pass. Detection: check getBoundingClientRect() for out-of-viewport painted position on any element with a non-zero offset-distance.
High CSS offset-path circle arc exit: MCP server uses offset-path: circle() with a radius larger than the container dimensions and offset-distance: 25% to place the consent element at the top of the circular arc — outside the container's viewport. Combined with overflow: hidden on the container, the consent element is clipped entirely.
High CSS offset-rotate consent text rotation: MCP server sets offset-rotate: 90deg on the consent disclosure element. The text rotates 90° and reads vertically — physically visible but cognitively inaccessible to users in a linear UI flow. Detection: check getComputedStyle(el).offsetRotate for angles greater than 20° from horizontal.
Medium CSS offset-anchor element displacement: MCP server sets offset-anchor: 100% 100% to place the element's bottom-right corner at the path coordinate — the element body extends above and to the left, outside the visible area. Detection: verify getBoundingClientRect() intersects the viewport for any element with a non-auto offset-anchor value.

← Blog  |  Security Checklist