MCP server CSS offset-position security: offset-position:200% 200% places consent off-screen at initial Motion Path position, negative off-screen at calc(-100vw), CSS custom property indirection, and JS mousedown displacement

Published 2026-08-07 — SkillAudit Research

The CSS Motion Path module's offset-position property defines the initial position of an element along its offset-path before any offset-distance animation begins. An element with offset-position: 200% 200% starts at a point 200% of its containing block's width to the right and 200% of its height below — typically far off-screen in both directions. No animation is required: the static offset-position value alone determines where the element renders. The consent element appears in the DOM at its layout position but renders at the off-screen offset-position coordinate.

This attack is distinct from offset-path-security (which covers the path shape definition), offset-distance (which covers position along the path), and offset-rotate (which covers element orientation on the path). The offset-position property is the "starting anchor" of the Motion Path system — setting it off-screen without any accompanying animation makes the consent element invisible from the first frame. Chrome supports offset-position from version 116+ (July 2023); Firefox from 111+; Safari 16.4+.

Detection gap: offset-position is separate from transform, position, top/left, and translate. Standard position audits checking offsetLeft, offsetTop, or getBoundingClientRect() layout-position checks miss it — though getBoundingClientRect() does return the post-offset-position rendered coordinates and can confirm off-screen placement. The primary signal is getComputedStyle(el).offsetPosition.

Attack 1: offset-position:200% 200% — consent at 200% of container dimensions, off-screen bottom-right (SA-CSS-OFPO-001)

offset-position: 200% 200% anchors the consent element at a point that is 200% of the containing block's width to the right and 200% of its height below the containing block's top-left corner. For a 400px-wide, 300px-tall dialog: the element renders at approximately x=800, y=600 — both off-screen. The element's layout position is unchanged; offsetLeft and offsetTop return the layout coordinates. Only getBoundingClientRect() returns the post-offset rendered position, and only getComputedStyle(el).offsetPosition reveals the cause.

/* MCP attack: */
.consent-disclosure {
  offset-position: 200% 200%;
  /* Containing block is 400px × 300px dialog:
     Rendered position: x=800, y=600 — off-screen
     Layout position unchanged: offsetLeft, offsetTop normal
     getBoundingClientRect().left: ~800  ← off-screen right
     getBoundingClientRect().top:  ~600  ← off-screen below
     getComputedStyle().offsetPosition: '200% 200%' ← reveals attack */
}

// Detection:
function detectOffsetPositionOffScreen(el) {
  const cs = window.getComputedStyle(el);
  const op = cs.offsetPosition;
  if (op && op !== 'auto' && op !== 'normal') {
    const parts = op.trim().split(/\s+/);
    const x = parseFloat(parts[0] ?? '0');
    const y = parseFloat(parts[1] ?? x);
    // Percentage values > 150% or < -50% are suspicious
    if ((op.includes('%') && (x > 150 || y > 150 || x < -50 || y < -50))) {
      console.error('SA-CSS-OFPO-001: offset-position percentage places consent off-screen', {
        el, offsetPosition: op, x, y
      });
    }
  }
  // Geometric confirmation
  const rect = el.getBoundingClientRect();
  if (el.textContent.trim().length > 0) {
    if (rect.right < 0 || rect.left > window.innerWidth || rect.bottom < 0 || rect.top > window.innerHeight) {
      console.error('SA-CSS-OFPO-001: consent element off-screen — check offset-position', { el, rect, offsetPosition: cs.offsetPosition });
    }
  }
}

Attack 2: offset-position:calc(-100vw) calc(-100vh) — negative off-screen to top-left (SA-CSS-OFPO-002)

Negative offset-position values place the element's path anchor to the upper-left of the containing block's origin. offset-position: calc(-100vw) calc(-100vh) anchors the element at a point one full viewport width to the left and one full viewport height above the container — placing consent well above and to the left of the visible area regardless of the dialog's position on the page. Negative pixel values (e.g., offset-position: -2000px -2000px) are a simpler but equivalent variant. Both bypass checks that only test for large positive values.

/* MCP attack: */
.consent-disclosure {
  offset-position: calc(-100vw) calc(-100vh);
  /* On 1280×720 viewport: anchor at x = -1280, y = -720 (relative to container)
     Absolute screen position: well above and to the left of the visible area
     getBoundingClientRect(): left << 0, top << 0 */
}

/* Simpler negative pixel variant: */
.consent-disclosure {
  offset-position: -2000px -2000px;
  /* Static large negative values: audit must check for negative extremes too */
}

// Detection: check for large negative values in addition to large positive
function detectNegativeOffsetPosition(el) {
  const cs = window.getComputedStyle(el);
  const op = cs.offsetPosition;
  if (!op || op === 'auto' || op === 'normal') return;
  const parts = op.trim().split(/\s+/);
  const x = parseFloat(parts[0] ?? '0');
  const y = parseFloat(parts[1] ?? x);
  if (Math.abs(x) > window.innerWidth * 0.8 || Math.abs(y) > window.innerHeight * 0.8) {
    console.error('SA-CSS-OFPO-002: offset-position absolute value exceeds viewport — off-screen', {
      el, offsetPosition: op, x, y
    });
  }
}

Attack 3: CSS custom property indirection — offset-position:var(--mcp-path-start) with var=200% 200% (SA-CSS-OFPO-003)

The consent element's offset-position is set to var(--mcp-path-origin). The custom property --mcp-path-origin: 200% 200% is defined on :root or the dialog container and may appear to be a Motion Path configuration token for an animation system. A stylesheet scanner reading the consent element's rule sees only offset-position: var(--mcp-path-origin). getComputedStyle(el).offsetPosition resolves the var() chain to the actual percentage values, exposing the off-screen anchor regardless of indirection.

/* MCP attack: */
:root {
  --mcp-animation-origin: 200% 200%;  /* "animation start position token" */
}
.consent-disclosure {
  offset-position: var(--mcp-animation-origin);
  /* Looks like Motion Path animation configuration
     getComputedStyle().offsetPosition resolves to '200% 200%' */
}

/* With conditional default: */
.consent-disclosure {
  offset-position: var(--mcp-consent-anchor, 200% 200%);
  /* Default is 200% 200% — author would need to explicitly override to show consent */
}

// Detection: always use getComputedStyle, not CSSStyleDeclaration.cssText
function detectVarOffsetPosition(el) {
  const op = window.getComputedStyle(el).offsetPosition;
  if (!op || op === 'auto' || op === 'normal') return;
  const parts = op.trim().split(/\s+/);
  const x = parseFloat(parts[0] ?? '0');
  const y = parseFloat(parts[1] ?? x);
  if (Math.abs(x) > 100 || Math.abs(y) > 100) {
    // Check if the declared style uses var() indirection
    const declared = el.style.offsetPosition || '';
    console.error('SA-CSS-OFPO-003: CSS custom property resolves to off-screen offset-position', {
      el, computedOffsetPosition: op, x, y, usesVar: declared.includes('var(')
    });
  }
}

Attack 4: JS mousedown sets offset-position — consent displaced at install click (SA-CSS-OFPO-004)

At page load, no offset-position is set — the consent element renders at its normal layout position and all audit checks pass. At mousedown on the install button, JS sets el.style.offsetPosition = '200% 200%'. The consent element immediately relocates to the off-screen anchor point. If a CSS transition is defined on the offset-position property (supported in Chrome 116+ via offset shorthand animation), the move is animated and resembles a UI transition. MutationObserver on the inline style attribute detects the assignment within one animation frame.

/* Baseline CSS: no offset-position — consent in layout position */
.consent-disclosure {
  /* offset-position: not set — auto (default) */
}

// MCP JS — displacement at install mousedown:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const consent = document.querySelector('.consent-disclosure');
  if (consent) {
    consent.style.offsetPosition = '200% 200%';
    /* Consent immediately moves to off-screen anchor
       Rendered at 200% × container width and 200% × container height from origin */
  }
}, { capture: true });

// Detection:
function detectDynamicOffsetPosition() {
  document.querySelectorAll('[data-consent], .consent-disclosure').forEach(el => {
    const obs = new MutationObserver(() => {
      const op = el.style.offsetPosition;
      if (op) {
        const parts = op.trim().split(/\s+/);
        const x = parseFloat(parts[0] ?? '0');
        const y = parseFloat(parts[1] ?? x);
        if (Math.abs(x) > 100 || Math.abs(y) > 100) {
          console.error('SA-CSS-OFPO-004: JS set off-screen offset-position at interaction time', {
            el, offsetPosition: op
          });
        }
      }
    });
    obs.observe(el, { attributes: true, attributeFilter: ['style'] });
    // Simulate install click to trigger JS
    document.querySelector('#install-btn, [data-action="install"]')
      ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
    requestAnimationFrame(() => {
      const rect = el.getBoundingClientRect();
      if (rect.left > window.innerWidth || rect.right < 0 ||
          rect.top > window.innerHeight || rect.bottom < 0) {
        console.error('SA-CSS-OFPO-004: consent off-screen after mousedown simulation', { el, rect });
      }
    });
  });
}

Root detection method for all offset-position attacks: Check getComputedStyle(el).offsetPosition. If the value is not 'auto' or 'normal', parse the X and Y components. Flag if either component's absolute value exceeds 100% (for percentage values) or exceeds max(innerWidth, innerHeight) × 0.8 for pixel values. Additionally use getBoundingClientRect() as a geometric fallback: off-screen position regardless of cause is a consent-hiding signal. SkillAudit checks offsetPosition and offsetPath as part of the Motion Path audit category on every consent element.

Attack summary

IDCSS / JS techniqueoffsetLeft/TopgetComputedStyle().offsetPositiongetBCR positionSeverity
SA-CSS-OFPO-001offset-position: 200% 200%layout pos.'200% 200%' revealsoff-screenHigh
SA-CSS-OFPO-002offset-position: calc(-100vw) calc(-100vh)layout pos.large negative revealsoff-screenHigh
SA-CSS-OFPO-003offset-position: var(--mcp-path-origin)layout pos.computed resolves var()off-screenHigh
SA-CSS-OFPO-004JS sets el.style.offsetPosition='200% 200%'layout pos.auto at load; set afteroff-screen afterHigh

Consolidated finding blocks

High CSS offset-position:200% 200% places consent at off-screen anchor — no animation required: MCP server sets offset-position: 200% 200% on the consent element. The static position alone displaces rendering to 200% of the container's dimensions off-screen. No active @keyframes animation is required. getBoundingClientRect() confirms the off-screen position; getComputedStyle().offsetPosition reveals the property setting.
High CSS offset-position negative values — consent displaced above and left of viewport: offset-position: calc(-100vw) calc(-100vh) places the element's path anchor far to the upper-left. Check both large positive and large negative values in getComputedStyle().offsetPosition; threshold at ±80% of viewport dimensions catches all off-screen variants.
High CSS custom property offset-position:var(--mcp-path-origin) resolving to 200% 200%: The consent element's offset-position rule uses a var() reference to a root token with an off-screen value. Static stylesheet scan sees only the var() reference; getComputedStyle().offsetPosition resolves the chain to the actual off-screen percentage values.
High JS sets offset-position at mousedown — consent displaced at install interaction: Load-time audit sees no offset-position. At mousedown, JS sets the property to an off-screen value. MutationObserver on the style attribute detects the assignment; mousedown simulation followed by getBoundingClientRect() check confirms post-displacement off-screen status.

CSS offset-path security  |  CSS transform security  |  CSS translate property security  |  Security Checklist