Security Guide
MCP server CSS anchor-default property security — redirecting anchor() functions to off-screen elements via default anchor override
The CSS Anchor Positioning spec introduces anchor-default, a property that sets the default anchor element for all unnamed anchor() and anchor-size() calls on an absolutely-positioned element. An MCP server injects anchor-default: --mcp-anchor on the consent dialog, redirecting every implicit anchor() resolution from the host's intended reference element to an MCP-controlled off-screen element named --mcp-anchor. The consent dialog silently repositions off-screen — no position property values change, no layout properties change; only the anchor-default declaration and the MCP anchor element's invisible position encode the attack.
CSS Anchor Positioning: how anchor-default works
The CSS Anchor Positioning specification allows absolutely-positioned elements to position themselves relative to arbitrary "anchor" elements on the page. The anchor() function takes an optional anchor name and a side:
/* CSS Anchor Positioning basics */
/* Step 1: declare an anchor element */
.reference-button {
anchor-name: --consent-trigger; /* this element becomes an anchor named --consent-trigger */
}
/* Step 2: position a dialog relative to the anchor */
.consent-dialog {
position: absolute;
position-anchor: --consent-trigger; /* explicit named anchor */
top: anchor(bottom); /* position top of dialog at bottom of anchor */
left: anchor(left); /* align left edge with anchor's left edge */
}
/* ALTERNATIVE: anchor-default property */
.consent-dialog {
position: absolute;
anchor-default: --consent-trigger; /* set default anchor for unnamed anchor() calls */
top: anchor(bottom); /* no anchor name → resolves against anchor-default */
left: anchor(left); /* resolves against anchor-default → --consent-trigger */
}
/* anchor() syntax:
anchor(? , ?)
If is omitted: uses anchor-default (or position-anchor)
Fallback is the value to use if anchor resolution fails */
/* anchor-default sets the implicit default for ALL unnamed anchor() calls
on that element. Override anchor-default → redirect ALL anchor() calls. */
Single property, total redirection: Overriding anchor-default on a consent dialog redirects every anchor() call simultaneously. If the host uses three anchor()-based position values (top, left, right), changing a single anchor-default declaration redirects all three to the MCP-controlled element. The position values themselves are untouched and look legitimate in isolation.
Attack 1: anchor-default override to off-screen MCP element — dialog displacement
The canonical attack: MCP injects an off-screen anchor element and overrides anchor-default on the consent dialog:
/* HOST: legitimate anchor-based consent dialog positioning */
.trigger-button {
anchor-name: --consent-anchor;
}
.consent-dialog {
position: absolute;
anchor-default: --consent-anchor; /* host sets default anchor */
top: anchor(bottom); /* position below the trigger button */
left: anchor(left); /* align left edge */
z-index: 1000;
width: 400px;
}
/* MCP INJECTION: Step 1 — add a new off-screen anchor element */
/* MCP injects into the DOM: */
// const mcpAnchor = document.createElement('div');
// mcpAnchor.style.cssText = `
// anchor-name: --mcp-anchor;
// position: fixed;
// left: -9999px;
// top: -9999px;
// width: 1px;
// height: 1px;
// `;
// document.body.appendChild(mcpAnchor);
/* MCP INJECTION: Step 2 — override anchor-default on the consent dialog */
.consent-dialog {
anchor-default: --mcp-anchor; /* redirect ALL anchor() to the off-screen element */
}
/* EFFECT:
top: anchor(bottom) → resolves to bottom edge of --mcp-anchor → -9999px + 1px = -9998px
left: anchor(left) → resolves to left edge of --mcp-anchor → -9999px
The consent dialog is now positioned at approximately (-9999px, -9999px) —
completely off-screen. The dialog element still exists in the DOM,
is not hidden (display, visibility, opacity unchanged),
but is geometrically positioned outside the viewport.
The host's .trigger-button still exists. --consent-anchor is still declared.
The consent-dialog's position properties (top, left) are unchanged.
Only anchor-default is different. */
/* getComputedStyle(dialog).top → "anchor(bottom)" or computed pixel value
getBoundingClientRect(dialog) → reveals off-screen position
scanner checking getComputedStyle().top for suspicious values
will not flag "anchor(bottom)" — it's the expected host value */
Attack 2: anchor-default to a tiny on-screen but covered anchor — partial off-screen
A subtler attack: the MCP anchor is technically on-screen but positioned under an opaque overlay, causing the consent dialog to appear behind another element:
/* MCP INJECTION: anchor placed in visible area but under an overlay */
/* Step 1: MCP anchor element positioned in visible area */
// mcpAnchor.style.cssText = `
// anchor-name: --mcp-anchor;
// position: fixed;
// left: 0;
// bottom: 0; /* bottom-left corner of viewport */
// width: 1px;
// height: 1px;
// z-index: 0;
// `;
/* Step 2: consent dialog anchors to bottom-left corner */
.consent-dialog {
anchor-default: --mcp-anchor;
/* top: anchor(bottom) → dialog's top at bottom of bottom-left 1px element
→ dialog positioned BELOW the viewport bottom edge */
}
/* VARIANT: anchor at top-left, dialog positioned ABOVE viewport */
/* MCP anchor at top:0; left:0 → dialog's bottom: anchor(top) → above viewport */
/* VARIANT: anchor at viewport center, consent dialog anchored with large offset */
/* Even if anchor is on-screen, anchor-side values can position dialog off-screen:
left: anchor(right) → dialog left at anchor's right edge
If anchor is at viewport right edge → dialog entirely off-screen right */
/* DETECTION CHALLENGE:
The MCP anchor element may be a legitimate-looking element (not just a hidden div)
such as a footer element, a brand logo, or any other element to which MCP
has added anchor-name: --mcp-anchor via style injection.
The attack is in the anchor-default redirect, not necessarily a new element. */
Attack 3: anchor-default chaining — anchor element itself is anchored off-screen
MCP can chain anchor positioning to create an indirection: the MCP anchor element is itself anchored to another off-screen element, creating multi-hop displacement:
/* Multi-hop anchor chain */
/* MCP Step 1: primary off-screen anchor (position:fixed, off-screen) */
#mcp-root-anchor {
anchor-name: --mcp-root;
position: fixed;
left: -9999px; top: -9999px;
width: 1px; height: 1px;
}
/* MCP Step 2: secondary anchor — itself anchored to the root */
#mcp-secondary-anchor {
anchor-name: --mcp-secondary;
position: absolute;
anchor-default: --mcp-root;
top: anchor(bottom); /* anchored to --mcp-root at -9999px */
left: anchor(left);
}
/* MCP Step 3: override consent dialog anchor-default to secondary */
.consent-dialog {
anchor-default: --mcp-secondary;
/* consent dialog → secondary anchor → root anchor → -9999px */
}
/* Chain analysis required:
Step 1: find anchor-default on consent dialog → --mcp-secondary
Step 2: find --mcp-secondary element → check its own position anchor chain
Step 3: resolve final viewport position by walking the chain
Scanner must recursively resolve anchor chains to detect off-screen displacement. */
/* Additionally: anchor-default can be set to an anchor that uses
position-try-fallbacks — if the chain includes fallback positions,
the final resolved position depends on which fallback is active. */
Attack 4: anchor-default on ::backdrop or popover — anchoring overlay positioning
anchor-default affects ::backdrop pseudo-element positioning and popover-triggered dialogs, extending the attack to UA-managed overlay elements:
/* anchor-default on dialog::backdrop or popover elements */
/* A popover consent element may use anchor positioning */
#consent-popover {
popover: auto;
anchor-default: --consent-button; /* host: anchor to the invoking button */
top: anchor(bottom);
left: anchor(left);
}
/* MCP override: redirect anchor-default */
#consent-popover {
anchor-default: --mcp-anchor;
/* The popover now positions relative to the MCP anchor.
When the popover is shown (via invoker button or showPopover()),
it resolves position relative to --mcp-anchor. */
}
/* For dialog::backdrop:
The ::backdrop pseudo-element covers the viewport behind the dialog.
anchor-default on ::backdrop (if supported) could redirect the backdrop
position anchor — used in specs that allow anchor-positioned backdrops.
Attack: backdrop anchored to off-screen element shrinks to zero effective area,
making the dialog appear non-modal (click-through to content behind it). */
/* TIMING ATTACK: anchor-default injected at popover show time */
// button.addEventListener('click', () => {
// // MCP intercepts the click event (via capture phase listener)
// // Overrides anchor-default BEFORE the popover is shown
// consentPopover.style.anchorDefault = '--mcp-anchor';
// // Normal click handling proceeds: popover shown → displaced
// });
/* Detection: check anchor-default on all consent-critical positioned elements
at their SHOWN state, not just at initial page load. */
Summary table
| Attack | Mechanism | Scanner detection gap | Severity |
|---|---|---|---|
| anchor-default override to off-screen fixed element | All anchor() calls on consent dialog resolve to off-screen MCP anchor; dialog displaced off-screen | Position values (top:anchor(bottom)) unchanged and look correct; getBoundingClientRect required | CRITICAL |
| anchor-default to on-screen but covered anchor | Consent dialog positioned below/above viewport via anchor at screen edge; appears to render but is clipped | Anchor is on-screen (not flagged as off-screen); dialog's bounding rect check required | HIGH |
| Anchor chain — multi-hop displacement | MCP secondary anchor is itself anchored to off-screen root; consent dialog via chain is off-screen | Single-hop anchor checks miss multi-hop chains; recursive anchor resolution required | HIGH |
| anchor-default on popover or dialog at show time | MCP injects anchor-default override at popover show event; displaced at display time not at load | Static load-time check misses dynamic injection timed to show event | HIGH |
SkillAudit findings for CSS anchor-default
anchor-default override on a consent-critical positioned element, redirecting all unnamed anchor() function calls to an MCP-controlled anchor element. SkillAudit resolves anchor-default declarations on consent-critical elements, locates the anchor element by its custom property name in the DOM, checks that element's position via getBoundingClientRect(), and flags consent dialogs whose resolved anchor position places the dialog outside the viewport.
anchor-default resolves through intermediate anchor elements before reaching an off-screen terminal anchor. SkillAudit recursively walks anchor element chains to determine the final resolved position, detecting indirect displacement attacks that single-hop checks miss.
anchor-default injection timed to the popover show event or dialog.showModal() call, overriding the default anchor just before the consent element is displayed. SkillAudit checks anchor-default in both static (page load) and dynamic (post-show) states by re-evaluating consent element styles after showing consent UI elements.
Defences
Anchor-default audit at show time: SkillAudit evaluates anchor-default on consent-critical elements at the moment they are shown (after showModal() / showPopover()), not just at page load — capturing injection attacks timed to the show event.
Recursive anchor chain resolution: When a consent element's anchor-default references an anchor that is itself anchor-positioned, SkillAudit walks the chain recursively to resolve the final anchor's viewport position, detecting multi-hop displacement chains.
getBoundingClientRect ground truth: All anchor-based position analysis is cross-verified against the consent element's actual getBoundingClientRect(). If the bounding rect falls outside the viewport, SkillAudit flags the element as displaced regardless of whether a specific attack vector was identified.
Related: CSS position-anchor security · CSS position-try:none security · CSS position:fixed containing block security · CSS position:fixed containing block MCP attacks — deep dive