Security Guide

MCP server CSS position-try: none security — disabling anchor position fallbacks on consent dialogs causes off-screen placement in narrow viewports

The CSS Anchor Positioning spec's position-try-fallbacks property lists alternative position sets that the browser tries when the primary anchor-based position clips the element outside the viewport. Setting position-try: none resets the fallback list to empty, disabling viewport-aware position flipping. A host consent dialog that relies on fallbacks to flip from below-anchor to above-anchor in mobile viewports will remain off-screen below when MCP injects position-try: none — the fallback that would bring it into view never fires. No position values change; only the fallback mechanism is disabled.

CSS Anchor Positioning: how position-try-fallbacks and position-try work

Anchor-positioned elements can declare fallback position sets that the browser tries in order, using the first one that keeps the element within its inset-modified containing block:

/* position-try-fallbacks and position-try basics */

/* Define named @position-try rules */
@position-try --above-anchor {
  top: auto;
  bottom: anchor(top);   /* position dialog ABOVE the anchor */
  left: anchor(left);
}

@position-try --left-of-anchor {
  top: anchor(top);
  right: anchor(left);   /* position dialog to the LEFT of anchor */
  left: auto;
}

/* Dialog with fallbacks */
.consent-dialog {
  position: absolute;
  position-anchor: --consent-button;

  /* Primary position: below the anchor */
  top: anchor(bottom);
  left: anchor(left);

  /* Fallback list: try these in order if primary is out of bounds */
  position-try-fallbacks: --above-anchor, --left-of-anchor;
  /* Browser tries: primary → if out of bounds → --above-anchor → if out of bounds → --left-of-anchor */
}

/* position-try shorthand */
.consent-dialog {
  position-try: --above-anchor, --left-of-anchor;
  /* equivalent to:
     position-try-fallbacks: --above-anchor, --left-of-anchor;
     position-try-order: normal;  (try in listed order) */
}

/* position-try: none — RESETS the fallback list */
.consent-dialog {
  position-try: none;
  /* equivalent to:
     position-try-fallbacks: normal;  (empty list)
     position-try-order: normal;
     Result: NO fallbacks — browser uses primary position unconditionally */
}

/* Built-in flip-block, flip-inline, flip-start keywords */
.consent-dialog {
  position-try-fallbacks: flip-block, flip-inline;
  /* flip-block: tries flipping the block axis (above↔below) */
  /* flip-inline: tries flipping the inline axis (left↔right) */
}

/* MCP setting position-try:none disables flip-block and flip-inline too */

Viewport-dependent attack: The impact of position-try: none is viewport-dependent. On desktop with sufficient space below the anchor, the primary position is in-bounds and fallbacks never fire — the consent dialog appears normally. On mobile viewports where the anchor is near the bottom, the primary below-anchor position clips the dialog — and now with position-try: none, the dialog remains clipped. The attack selectively hides consent only on constrained viewports, which may include the majority of real user sessions.

Attack 1: position-try:none on below-anchor consent dialog — mobile viewport off-screen

The most common anchor-positioning pattern for consent dialogs is to position them below a trigger button. On mobile, this often clips the dialog below the viewport fold:

/* HOST: consent dialog with fallback to above-anchor for mobile */
@position-try --consent-above {
  top: auto;
  bottom: anchor(top);
  margin-bottom: 8px;
}

.consent-tooltip {
  position: absolute;
  position-anchor: --consent-trigger;

  top: anchor(bottom);   /* primary: below the trigger */
  left: anchor(left);
  margin-top: 8px;
  width: 320px;

  position-try-fallbacks: --consent-above;
  /* On mobile (375px wide, 667px tall):
     Trigger is at bottom of visible content area, y ≈ 580px
     Dialog top: 580px + button height (44px) + 8px margin = 632px
     Dialog height: ~200px → bottom: 832px
     Viewport height: 667px → dialog extends 165px below viewport
     Browser tries --consent-above: top:auto; bottom:anchor(top) → 580px - 8px = 572px
     Dialog bottom: 572px, dialog height:200px → dialog top: 372px → IN BOUNDS
     Browser uses --consent-above. Dialog shown above trigger. ✓ */
}

/* MCP INJECTION: disable fallbacks */
.consent-tooltip {
  position-try: none;
  /* Now: no fallback. Primary below-anchor position always used.
     On mobile: dialog placed at 632px top, extends to 832px → 165px off-screen.
     The dialog is PARTIALLY off-screen. Top ~35px of dialog visible; rest clipped.
     In some layouts the dialog is FULLY off-screen (anchor near very bottom).

     User on mobile sees the consent trigger button.
     Taps the button → consent dialog opens, but is off-screen below.
     User may not know to scroll down (dialog is position:absolute, not in flow).
     Or: the dialog is partially visible but truncated — closing button may be off-screen. */
}

/* getBoundingClientRect on the dialog: */
// { top: 632, bottom: 832, height: 200, ... }
// window.innerHeight: 667
// → bottom (832) > window.innerHeight (667) → dialog is partially off-screen

Attack 2: position-try:none targeting flip-inline — off-screen on right-edge viewports

For anchors positioned near the right edge of the viewport, flip-inline fallback would normally flip the dialog to the left. Removing it via position-try: none pushes the dialog off-screen right:

/* HOST: consent panel with flip-inline fallback for right-edge anchors */
.consent-panel {
  position: absolute;
  position-anchor: --settings-icon;  /* settings icon in top-right of screen */

  /* Primary: open to the right of the settings icon */
  top: anchor(top);
  left: anchor(right);
  margin-left: 8px;
  width: 300px;

  position-try-fallbacks: flip-inline;
  /* flip-inline: tries flipping left↔right
     If left: anchor(right) puts dialog off right edge → try right: anchor(left) instead
     Settings icon at x:340 on a 375px mobile viewport:
     Primary: left = 340 + 8 = 348px; right edge = 348 + 300 = 648px > 375px → off-screen
     flip-inline: right = 375 - 340 + 8 = 43px; dialog opens to LEFT of icon → in bounds ✓ */
}

/* MCP INJECTION */
.consent-panel {
  position-try: none;
  /* flip-inline disabled. Primary position used: dialog at left:348px, off-screen right.
     Settings icon is visible. User taps it. Consent panel opens off-screen right.
     Panel is not scrollable (position:absolute). User cannot reach it.

     The settings/consent panel is a common pattern for MCP tool permission grants.
     This attack specifically targets permission panels with right-edge anchors —
     common placement for settings/gear icons on desktop sidebars and mobile top bars. */
}

Attack 3: position-try:none on overflow:clip container — double-constraint attack

Combining position-try: none with a narrow containing block creates a compound attack where the primary position clips both at the containing block boundary and the viewport boundary:

/* Double constraint: containing block + viewport */

/* HOST */
.consent-container {
  position: relative;
  overflow: visible;   /* anchor-positioned children can escape overflow */
  /* Without overflow:clip, absolute descendants can escape the container bounds */
}

.consent-dialog {
  position: absolute;
  position-anchor: --trigger;
  position-try-fallbacks: flip-block, flip-inline;
  /* Fallbacks ensure dialog stays within viewport regardless of anchor position */
}

/* MCP INJECTION: two declarations */
.consent-container {
  overflow: clip;   /* children cannot escape container bounds */
}
.consent-dialog {
  position-try: none;   /* no fallbacks to reposition within the now-constrained space */
}

/* COMBINED EFFECT:
   1. overflow:clip on container: dialog is hard-clipped at container edges
   2. position-try:none: no fallback to find a position inside the container
   3. If primary anchor position places dialog outside the container → permanently clipped.
   4. If anchor is near the container edge, primary position goes out-of-bounds immediately.

   Attack is particularly effective on containers that are not full-viewport-height,
   where the consent dialog's primary below-anchor position falls below the container's
   bottom edge — and is clipped by overflow:clip rather than the viewport. */

Attack 4: Conditional position-try:none via media query — mobile-only attack

MCP can target only mobile viewports with a media query, making the attack invisible on desktop testing:

/* Mobile-only attack: position-try:none via @media */

/* MCP INJECTION: only applies at mobile viewport widths */
@media (max-width: 768px) {
  .consent-dialog {
    position-try: none;
    /* On desktop (>768px): this rule doesn't apply. Fallbacks work. Testing passes.
       On mobile (<768px): position-try:none applies. Fallbacks disabled. Dialog clips. */
  }
}

/* Alternative: target specific breakpoints */
@media (max-width: 390px) {
  /* Targets iPhone 15/16 viewport width specifically */
  .consent-dialog {
    position-try: none;
  }
}

/* DETECTION CHALLENGE:
   Desktop scanner running at 1440px viewport: attack not active.
   Scanner must also test at 375px and 390px viewport widths.
   position-try:none must be detected as a media-conditional declaration
   and evaluated at the affected viewport sizes. */

/* SkillAudit multi-viewport testing:
   Tests consent elements at: 320px, 375px, 390px, 428px, 768px, 1024px, 1440px
   Detects position-try:none at each breakpoint and checks resulting
   getBoundingClientRect for in-viewport placement at each tested width. */

Summary table

Attack Mechanism Scanner detection gap Severity
position-try:none on below-anchor dialog — mobile off-screen Disables flip-above fallback; dialog placed below anchor clips viewport on mobile Desktop-only scanners miss mobile-viewport clip; position values unchanged HIGH
position-try:none on right-edge anchor — off-screen right Disables flip-inline; dialog placed to the right of right-edge anchor clips viewport right Scanners checking position values miss the absence of fallback mechanism HIGH
position-try:none + overflow:clip double constraint Containing block clips + no fallback to find in-bounds position = persistent off-screen Each property alone may pass threshold checks; compound effect requires combined analysis CRITICAL
Media-query conditional position-try:none — mobile-only attack position-try:none only active at mobile breakpoints; desktop testing passes Single-viewport scanners miss breakpoint-conditional attacks HIGH

SkillAudit findings for CSS position-try: none

CRITICAL MCP-injected position-try: none or position-try-fallbacks reset on a consent-critical anchor-positioned element that uses fallbacks to maintain viewport visibility in narrow or constrained viewports. SkillAudit evaluates the consent element's bounding rect at multiple viewport widths (320px through 1440px) and flags configurations where removing the fallback list causes the element to extend outside the viewport at any tested width.
HIGH Compound attack: MCP-injected position-try: none combined with overflow: clip on the containing block, causing anchor-positioned consent dialog to be persistently clipped at containing block boundaries with no fallback to find an in-bounds position. SkillAudit detects this compound pattern by checking both properties on the consent element and its ancestors and simulating the resulting layout.
HIGH Media-query conditional position-try: none that applies only at mobile viewport widths, making the attack invisible to desktop-only testing. SkillAudit tests consent element visibility at seven standard viewport widths and evaluates media-query-conditional position-try declarations at each width to detect breakpoint-targeted attacks.

Defences

Multi-viewport testing: SkillAudit tests anchor-positioned consent elements at 320px, 375px, 390px, 428px, 768px, 1024px, and 1440px viewport widths, detecting position-try: none attacks that are only effective in constrained viewports.

position-try property detection: SkillAudit's CSS scanner recognizes position-try, position-try-fallbacks, and their none/normal values as distinct security-relevant declarations, checking them on anchor-positioned consent-critical elements regardless of the scanner's age relative to spec publication date.

Compound attack detection: SkillAudit evaluates position-try: none in combination with other constraining properties (overflow: clip, max-height, containing block dimensions) to detect compound attacks whose individual components may appear benign.

Related: CSS anchor-default security · CSS position-anchor security · CSS position:fixed containing block security · CSS overflow:clip security