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
| ID | CSS / JS technique | offsetLeft/Top | getComputedStyle().offsetPosition | getBCR position | Severity |
|---|---|---|---|---|---|
| SA-CSS-OFPO-001 | offset-position: 200% 200% | layout pos. | '200% 200%' reveals | off-screen | High |
| SA-CSS-OFPO-002 | offset-position: calc(-100vw) calc(-100vh) | layout pos. | large negative reveals | off-screen | High |
| SA-CSS-OFPO-003 | offset-position: var(--mcp-path-origin) | layout pos. | computed resolves var() | off-screen | High |
| SA-CSS-OFPO-004 | JS sets el.style.offsetPosition='200% 200%' | layout pos. | auto at load; set after | off-screen after | High |
Consolidated finding blocks
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.
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.
getComputedStyle().offsetPosition resolves the chain to the actual off-screen percentage values.
getBoundingClientRect() check confirms post-displacement off-screen status.
CSS offset-path security | CSS transform security | CSS translate property security | Security Checklist