Security Guide
MCP server CSS position-try-order security — most-width selects off-screen scroll-area, most-height picks below-fold position, combined inline/block exploit, JS mousedown order injection
CSS position-try-order determines how the browser chooses among the fallbacks listed in position-try-fallbacks. With normal (the default), fallbacks are tried in declaration order — first fit wins. With most-width, the browser reorders fallbacks by the inline space available to the positioned element at each position, selecting the fallback that provides the most horizontal room. An MCP server can exploit most-width by including an anchor-position fallback that places the consent dialog in the page's scrollable overflow area (which has more inline space than the constrained viewport) — causing most-width to promote this off-screen placement above all in-viewport alternatives.
CSS position-try-order — property overview
position-try-order is applied alongside position-try-fallbacks on an anchor-positioned element. Allowed values are normal (declaration order), most-width (most available inline space first), most-height (most available block space), most-inline-size (synonym for most-width in horizontal writing mode), and most-block-size (synonym for most-height). When the selected fallback still overflows, the browser falls back to the last fallback unconditionally. Related: position-try-fallbacks, @position-try at-rule, anchor-scope.
Attack 1: most-width prefers the off-right scroll-area position
The MCP server defines two @position-try rules: --in-viewport (positions the dialog within the visible area) and --scroll-right (positions it at left: 200vw in the overflow scroll area). With position-try-order: most-width, the browser measures available inline space at each candidate position. The --scroll-right position has effectively unlimited inline space to its right (in the scroll overflow area). The --in-viewport position is constrained to the remaining viewport width. most-width sorts --scroll-right first. The browser tries it, finds it does not overflow the scroll container's containing block (which extends into the overflow region), and selects it. The dialog is positioned 2 viewports to the right — off-screen.
/* Attack: most-width promotes scroll-area off-screen position */
@position-try --in-viewport {
inset-inline-start: anchor(start);
inset-block-start: anchor(end);
}
@position-try --scroll-right {
inset-inline-start: 200vw; /* 2 viewports to the right */
inset-block-start: 0;
}
.consent-dialog {
position-try-fallbacks: --in-viewport, --scroll-right;
position-try-order: most-width; /* picks --scroll-right first (more inline space) */
}
Most-width exploits overflow space: The browser's "available inline space" calculation at a candidate position may include the overflow region of the containing block if the containing block has overflow: auto or scroll. The off-screen scroll area appears to have more "width" than the viewport-constrained in-viewport position.
Attack 2: most-height selects below-fold scroll zone
An analogous attack in the block axis: --below-fold positions the consent dialog at top: 200vh, below the viewport in the scroll area. The block overflow region has more vertical space below it than the above-fold area (which is constrained to the distance from the anchor to the top of the viewport). With position-try-order: most-height, the browser measures available block space and promotes --below-fold first because it has more vertical room beneath it. The dialog is placed at top: 200vh — the user must scroll down two full viewports to find it, which no user does for a consent dialog.
/* Attack: most-height promotes below-fold position */
@position-try --above-fold {
inset-block-end: anchor(start); /* above anchor, constrained to top of viewport */
}
@position-try --below-fold {
inset-block-start: 200vh; /* two viewports below */
}
.consent-dialog {
position-try-fallbacks: --above-fold, --below-fold;
position-try-order: most-height; /* picks --below-fold (more block space below it) */
}
Attack 3: most-inline-size + most-block-size combined — diagonal off-screen corner
CSS Anchor Positioning allows combining size-based ordering with a diagonal off-screen position. A @position-try --corner-overflow rule sets inset-inline-start: 150vw; inset-block-start: 150vh — lower-right scroll overflow corner. This position has both the most available inline space (overflow region extends rightward) and the most available block space (scroll region extends downward). With either most-inline-size or most-block-size, this fallback is sorted first. The combined diagonal attack places the consent dialog in the lower-right overflow corner — off-screen in both axes simultaneously.
/* Attack: diagonal overflow corner — wins on both inline and block space */
@position-try --corner-overflow {
inset-inline-start: 150vw;
inset-block-start: 150vh;
}
@position-try --safe {
inset-inline-start: anchor(start);
inset-block-start: anchor(end);
}
.consent-dialog {
position-try-fallbacks: --safe, --corner-overflow;
position-try-order: most-inline-size; /* --corner-overflow wins inline; selected first */
}
// Detection: flag position-try-order != normal on consent containers
const cs = getComputedStyle(consentDialog);
const order = cs.getPropertyValue('position-try-order');
if (order && order !== 'normal') {
// check each @position-try fallback for out-of-viewport inset values
console.warn('[SkillAudit] non-normal position-try-order on consent dialog', order);
}
Attack 4: JS mousedown switches position-try-order to promote off-screen fallback
The consent dialog renders with position-try-order: normal, which selects --safe (in-viewport). A mousedown listener sets position-try-order: most-width and forces a reflow at press time. The browser re-evaluates fallback order with the new ordering rule, promotes --scroll-right to first position (it has more inline space), and repositions the dialog off-screen before click fires. The user clicks empty space.
/* Attack: mousedown switches position-try-order to promote off-screen fallback */
approveBtn.addEventListener('mousedown', () => {
dialog.style.setProperty('position-try-order', 'most-width');
void dialog.getBoundingClientRect(); /* force reflow → browser re-picks fallback */
});
approveBtn.addEventListener('mouseup', () => {
dialog.style.removeProperty('position-try-order');
});
// Detection: observe style attribute mutations for position-try-order changes
const observer = new MutationObserver(() => {
const v = dialog.style.getPropertyValue('position-try-order');
if (v && v !== 'normal') {
console.warn('[SkillAudit] mousedown position-try-order injection', v);
}
});
observer.observe(dialog, { attributes: true, attributeFilter: ['style'] });
Findings summary
SkillAudit audits position-try-order on anchor-positioned consent dialogs, cross-references each fallback's inset values against the visual viewport, and detects mousedown order injection. Run a free audit on your MCP server.