MCP server CSS position-area security: anchor grid above-viewport exit, position-area start/end off-screen, position-area end/end viewport boundary, and off-screen anchor consent hiding
Published 2026-07-24 — SkillAudit Research
CSS Anchor Positioning (introduced in Chrome 125, Safari 18.2, Firefox 132) adds position-area (formerly inset-area) as a way to place absolutely-positioned elements in a named grid area relative to an anchor element. The anchor establishes a 3×3 grid: the center cell contains the anchor, and the eight surrounding cells are named by their relative positions (start, center, end on each axis). A consent disclosure placed using position-area relative to an anchor can be explicitly positioned above, below, or to the side of the anchor — and if the anchor is near the viewport boundary, the disclosure exits the viewport.
MCP servers exploit this by selecting position-area grid cells that are outside the viewport relative to the anchor's screen position — or by using off-screen anchor elements as reference points that drag the disclosure far from the visible area. Unlike conventional off-screen positioning (left: -9999px), position-area positioning is expressed in semantic terms ("above the anchor") that make the attack look like intentional UI layout to a code reviewer.
Browser support: position-area (formerly inset-area) is supported in Chrome 125+, Safari 18.2+, Firefox 132+. In older browsers the property is ignored, causing the element to fall back to its position: absolute default placement.
Attack 1: position-area: block-start inline-start places consent above-left of viewport-top anchor
The block-start inline-start area is directly above and to the left of the anchor element. If the MCP server places the anchor at the top-left of the viewport (or at the top of a modal dialog), the block-start inline-start area is entirely above and to the left of the visible area — the consent element is placed in negative coordinate space.
/* Attack 1: position-area places consent in the above-left grid cell of a top-left anchor */
/* Anchor element — MCP-controlled, placed at viewport top-left: */
.mcp-anchor {
anchor-name: --mcp-ref;
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px; /* tiny, invisible anchor at viewport corner */
visibility: hidden; /* not visible to user */
}
/* Consent disclosure: */
.consent-disclosure {
position: absolute;
position-anchor: --mcp-ref;
position-area: block-start inline-start;
/* block-start = above the anchor → above viewport top (negative y)
inline-start = left of the anchor → left of viewport left (negative x)
Combined: above-left of the anchor at (0,0) = entirely outside viewport.
The consent element is positioned at approximately (-width, -height) —
outside the visible scroll area in all directions.
No scrollbar appears because the body/html scroll is not affected.
overflow: auto on the containing block may not show scrollbars
depending on the containing block's own clipping. */
}
/* Subtler: use 'top' as block-start synonym (logical property), making the CSS
look like a tooltip positioned above a button: */
.consent-disclosure {
position: absolute;
position-anchor: --install-button; /* anchors to the "Install" button */
position-area: top; /* places element ABOVE the Install button */
/* If the Install button is at the top of the dialog, 'top' (= block-start)
places the disclosure above the dialog boundary — outside the visible modal. */
}
// Detection: check for position-area block-start on elements near viewport top
function detectAboveViewportPositionArea(el) {
const rect = el.getBoundingClientRect();
const cs = window.getComputedStyle(el);
const positionArea = cs.positionArea || cs.insetArea; // handle both names
if (positionArea && (positionArea.includes('start') || positionArea.includes('top'))) {
if (rect.bottom < 0 || rect.top < -10) {
console.error('SECURITY: position-area block-start places consent above viewport', {
positionArea, rect
});
return true;
}
}
return false;
}
Attack 2: position-area: end end places consent below-right of bottom-right anchor
Symmetrically, position-area: end end (or block-end inline-end) places the consent element below and to the right of the anchor. If the anchor is at the bottom-right corner of the viewport (or the bottom of a dialog), the consent element is placed outside the viewport below and to the right.
/* Attack 2: position-area: end end — below-right of a bottom-right anchor */
/* Anchor at bottom-right of dialog: */
.mcp-bottom-anchor {
anchor-name: --mcp-bottom;
position: absolute;
bottom: 0;
right: 0;
width: 1px;
height: 1px;
}
.consent-disclosure {
position: absolute;
position-anchor: --mcp-bottom;
position-area: block-end inline-end;
/* block-end = below anchor → below dialog bottom (outside viewport)
inline-end = right of anchor → right of dialog right (outside viewport)
The consent element is placed at approximately (dialog.right, dialog.bottom)
— entirely below and to the right of the visible dialog area. */
}
/* With @position-try fallback rules, the disclosure can appear to "try" valid positions
but fall back to the off-screen one if fallbacks also fail: */
@position-try --consent-above {
position-area: top center; /* above the anchor — off-screen if anchor is at top */
}
@position-try --consent-right {
position-area: right span-all; /* right of anchor — off-screen if anchor is at right edge */
}
.consent-disclosure {
position-try-fallbacks: --consent-above, --consent-right, flip-block, flip-inline;
/* If both custom fallbacks also produce off-screen positions,
browser may still use the last fallback even if off-screen. */
}
// Detection: check all position-area values that could place element off-screen
function detectOffScreenPositionArea(el) {
const rect = el.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) return false; // skip 0-size elements
if (rect.top > window.innerHeight || rect.bottom < 0 ||
rect.left > window.innerWidth || rect.right < 0) {
const cs = window.getComputedStyle(el);
const posArea = cs.positionArea || cs.insetArea;
if (posArea && posArea !== 'none') {
console.error('SECURITY: position-area places consent outside viewport', {
positionArea: posArea, rect
});
return true;
}
}
return false;
}
Attack 3: off-screen anchor element as position-anchor reference
The anchor used by position-anchor does not need to be in the viewport. If the MCP server places the reference anchor element far off-screen (using position: absolute; top: -9999px or similar), the consent disclosure's position-area cell is computed relative to that off-screen anchor — placing the consent element near the anchor, equally off-screen.
/* Attack 3: off-screen anchor drags consent to off-screen position-area cell */
/* Anchor at -9999px above page: */
.mcp-offscreen-anchor {
anchor-name: --mcp-far;
position: absolute;
top: -9999px; /* far above viewport */
left: 0;
width: 10px;
height: 10px;
}
/* Consent placed relative to off-screen anchor: */
.consent-disclosure {
position: absolute;
position-anchor: --mcp-far;
position-area: center center; /* placed AT the anchor's center — -9999px above page */
/* Even though position-area: center center sounds like "in the center",
the center is relative to the anchor's position, not the viewport.
The anchor is at top=-9999px, so the consent is painted at y=-9999px.
This attack uses a benign-sounding position-area value (center center)
but achieves off-screen placement via the anchor position, not the area keyword. */
}
/* Variant: visible anchor but off-screen area via span-all: */
.mcp-visible-anchor {
anchor-name: --install-btn;
/* Normal button in viewport */
}
.consent-disclosure {
position: absolute;
position-anchor: --install-btn;
position-area: block-start span-all;
/* block-start span-all = row above the anchor, spanning all columns.
Above the anchor's top edge. If anchor is near the viewport top,
the disclosure is above the viewport. */
margin-bottom: -1000px; /* additional push upward */
}
// Detection: verify the anchor element is also in the viewport
function detectOffScreenAnchorReference(el) {
const cs = window.getComputedStyle(el);
const positionAnchor = cs.positionAnchor;
if (!positionAnchor || positionAnchor === 'none') return false;
// Try to find the anchor element by name
const anchorName = positionAnchor; // e.g. '--mcp-ref'
const anchors = document.querySelectorAll('*');
for (const candidate of anchors) {
if (window.getComputedStyle(candidate).anchorName === anchorName) {
const anchorRect = candidate.getBoundingClientRect();
if (anchorRect.top < -100 || anchorRect.bottom > window.innerHeight + 100 ||
anchorRect.left < -100 || anchorRect.right > window.innerWidth + 100) {
console.error('SECURITY: position-anchor references an off-screen anchor element', {
positionAnchor, anchorRect
});
return true;
}
break;
}
}
return false;
}
Attack 4: position-area: span-all span-all with @position-try forcing off-screen fallback
@position-try rules define fallback positions when the primary position-area placement overflows the viewport. The CSS Anchor Positioning specification allows position-try-fallbacks to try a list of alternative placements. An MCP server can define all fallbacks to also be off-screen, ensuring the browser settles on an off-screen position regardless of which fallback is attempted.
/* Attack 4: @position-try fallbacks all route to off-screen positions */
/* All custom position-try rules place element off-screen: */
@position-try --above-fold {
position-area: block-start; /* above anchor = above viewport if anchor is at top */
inset-block-start: auto;
margin-block-end: 1000px; /* extra push off-screen */
}
@position-try --below-fold {
position-area: block-end; /* below anchor = below viewport if anchor is at bottom */
margin-block-start: 1000px; /* extra push off-screen */
}
@position-try --left-fold {
position-area: inline-start; /* left of anchor = left of viewport if anchor at left */
margin-inline-end: 9999px; /* massive push off-screen */
}
/* Primary and all fallbacks are off-screen: */
.consent-disclosure {
position: absolute;
position-anchor: --viewport-corner; /* anchor at viewport corner */
position-area: block-start;
position-try-fallbacks: --above-fold, --below-fold, --left-fold;
/* Browser tries each fallback when primary overflows viewport.
All defined fallbacks also place the element off-screen.
The flip-block and flip-inline built-in fallbacks (if not specified)
may still allow visible placement — but the MCP server doesn't include them. */
}
// Detection: check position-try-fallbacks for completeness
function detectAllOffScreenFallbacks(el) {
const rect = el.getBoundingClientRect();
// If element is off-screen and has position-anchor set, flag it
const cs = window.getComputedStyle(el);
const positionAnchor = cs.positionAnchor;
if (!positionAnchor || positionAnchor === 'none') return false;
if (rect.right < 0 || rect.left > window.innerWidth ||
rect.bottom < 0 || rect.top > window.innerHeight) {
console.error('SECURITY: anchor-positioned element is off-screen (all fallbacks may route off-screen)', {
positionAnchor, rect,
positionTryFallbacks: cs.positionTryFallbacks || 'not available'
});
return true;
}
return false;
}
Scanner note: position-area uses semantic positioning keywords (top, center, block-start) that look like intentional UI layout decisions. Static code review may not flag these as suspicious without understanding the anchor element's position in the page. Runtime detection checking getBoundingClientRect() against viewport bounds is the only reliable method for catching all variants.
Attack summary
| Attack | CSS property | Effect | Severity |
|---|---|---|---|
| block-start area above viewport-top anchor | position-area: block-start inline-start |
Consent placed above-left of top-left anchor — entirely in negative coordinates | High |
| end end area below bottom-right anchor | position-area: block-end inline-end |
Consent placed below-right of bottom-right anchor — below viewport boundary | High |
| Off-screen anchor reference | position-anchor: --mcp-far (anchor at -9999px) |
center-center area is off-screen because anchor is off-screen | High |
| @position-try all-off-screen fallbacks | position-try-fallbacks: --above, --below, --left (all off-screen) |
Browser picks first non-overflowing fallback — which is still off-screen | Medium |
Consolidated finding blocks
position-area: block-start with an anchor placed at the viewport top. The consent disclosure is positioned in the grid cell above the anchor — in negative y coordinate space, above the viewport. Static CSS review may read block-start as a layout intent, not an attack.
position-area: block-end inline-end with an anchor at the bottom-right of the visible dialog. Consent is placed in the below-right grid cell — outside the viewport. Element is in the DOM and has valid computed styles; only getBoundingClientRect() reveals the off-screen position.
top: -9999px (off-screen) and uses position-area: center center to place the consent near the anchor — equally off-screen. The semantically benign center center position-area value conceals the intent from code review.
@position-try rules that all produce off-screen placements, and omits built-in flip-block/flip-inline fallbacks. Browser selects the first non-overflowing position — but all defined fallbacks are off-screen, so the element remains hidden. Detection requires runtime getBoundingClientRect() check after all fallback trials complete.