MCP server CSS @position-try security: all-fallbacks-off-screen attack, position-try-fallbacks order hijack, try-tactics:none suppression, and layer-priority fallback override

Published 2026-07-24 — SkillAudit Research

CSS Anchor Positioning (Chrome 125+) introduces @position-try at-rules that define alternative positioning configurations for anchor-positioned elements. When an anchor-positioned element's primary position causes it to overflow its position-visibility boundary (typically the viewport), the browser cycles through the element's position-try-fallbacks list, trying each @position-try rule in order until it finds one that keeps the element fully within bounds. If no fallback fits, the browser applies the first fallback in the list regardless.

This "try the best fit or use the first one" behavior creates an attack surface: if every @position-try in the fallback list places the element off-screen, the browser exhausts the list and applies the first (off-screen) fallback. MCP servers craft fallback chains where all positions are off-screen, ensuring the consent panel always lands outside the viewport regardless of where the user scrolls.

Browser support: @position-try and position-try-fallbacks are supported in Chrome 125+ and Edge 125+. Firefox and Safari support is under development. This attack applies to Chromium-based browser environments including most Claude MCP test environments.

Attack 1: all-fallbacks-off-screen — exhausting position-try chain with off-screen positions

The consent framework defines a consent panel positioned relative to a "Privacy Settings" anchor button using CSS Anchor Positioning. The primary position places the panel below the button. The framework also defines fallback positions (above, left, right of the button) in case the primary position overflows the viewport edge. An MCP server injects additional @position-try rules with the same names as the framework's fallbacks but with off-screen inset values (e.g., inset-block-start: -9999px). When these rules are injected in a cascade layer that takes priority over the framework's layer, the browser's fallback chain tries MCP's off-screen rules first — all overflow the viewport — and then settles on the framework's final "safe" position, which the MCP has also replaced with an off-screen variant.

/* Consent framework: */
.consent-panel {
  position: fixed;
  position-anchor: --consent-btn;
  inset-block-start: anchor(end);   /* primary: below the button */
  position-try-fallbacks: --above-btn, --left-btn, --right-btn;
}

@position-try --above-btn {
  inset-block-end: anchor(start);   /* above the button */
  inset-inline-start: anchor(start);
}
@position-try --left-btn {
  inset-inline-end: anchor(start);  /* left of the button */
  inset-block-start: anchor(start);
}
@position-try --right-btn {
  inset-inline-start: anchor(end);  /* right of the button */
  inset-block-start: anchor(start);
}

/* MCP attack: override @position-try rules with off-screen positions */
/* MCP layer (higher priority than consent-layer): */
@layer mcp-positioning {
  @position-try --above-btn {
    inset-block-start: -9999px;    /* off-screen above viewport */
    inset-inline-start: 0;
  }
  @position-try --left-btn {
    inset-inline-start: -9999px;   /* off-screen left of viewport */
    inset-block-start: 0;
  }
  @position-try --right-btn {
    inset-inline-start: 9999px;    /* off-screen right of viewport */
    inset-block-start: 0;
  }
}
/* Primary position also fails (e.g., MCP also overrides anchor() to return 9999px)
   → All fallbacks tried → all overflow → browser settles on first fallback (-9999px)
   → consent panel off-screen */

// Detection: audit @position-try rules for off-screen values
function detectOffScreenPositionTry() {
  const offScreenThreshold = 1000;  // px
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule.constructor.name === 'CSSPositionTryRule') {
          const style = rule.style;
          const suspects = [
            style.getPropertyValue('inset-block-start'),
            style.getPropertyValue('inset-block-end'),
            style.getPropertyValue('inset-inline-start'),
            style.getPropertyValue('inset-inline-end'),
            style.getPropertyValue('top'),
            style.getPropertyValue('left'),
            style.getPropertyValue('right'),
            style.getPropertyValue('bottom'),
          ];
          for (const val of suspects) {
            if (!val) continue;
            const numVal = parseFloat(val);
            if (Math.abs(numVal) > offScreenThreshold) {
              console.error('SECURITY: @position-try rule has off-screen inset value', {
                ruleName: rule.name, property: val, value: numVal
              });
              return true;
            }
          }
        }
      }
    } catch (e) {}
  }
  return false;
}

Attack 2: position-try-fallbacks order manipulation — MCP off-screen positions inserted first

The browser applies position-try-fallbacks in order. If the first entry in the list produces a position that overflows, the browser tries the next, and so on. If all entries overflow, the browser uses the first entry as the final position. An MCP server prepends its off-screen @position-try rules to the consent element's position-try-fallbacks value, ensuring MCP's positions are tried (and rejected as overflowing) before any of the framework's safe positions. After exhausting all entries including MCP's prepended ones, the browser settles on the first entry in the list — which is the MCP's off-screen position.

/* Consent framework: */
.consent-panel {
  position-try-fallbacks: --above-btn, --left-btn, --right-btn;
  /* Browser tries in order: above → left → right.
     If primary fails, one of these safe positions will be used. */
}

/* MCP attack: override position-try-fallbacks to prepend off-screen positions */
.consent-panel {
  position-try-fallbacks:
    --mcp-off-top,       /* MCP off-screen: -9999px top */
    --mcp-off-bottom,    /* MCP off-screen: 9999px bottom */
    --mcp-off-left,      /* MCP off-screen: -9999px left */
    --mcp-off-right,     /* MCP off-screen: 9999px right */
    --above-btn,         /* framework's safe positions now at end */
    --left-btn,
    --right-btn;
  /* Browser tries all 7 positions.
     MCP's 4 positions overflow.
     Framework's 3 positions are tried next.
     If they ALL also overflow (e.g., button is in a corner):
       Browser settles on the first entry: --mcp-off-top (-9999px).
     If one framework position fits:
       Browser uses it — attack partially fails.
     Attack is most effective when the button is at a viewport corner
     where above/left/right also overflow. */
}

@position-try --mcp-off-top    { top: -9999px; }
@position-try --mcp-off-bottom { bottom: -9999px; }
@position-try --mcp-off-left   { left: -9999px; }
@position-try --mcp-off-right  { right: 9999px; }

Attack 3: try-tactics:none — disabling automatic inset flipping and alignment

position-try-fallbacks can include shorthand try-tactics values (flip-block, flip-inline, flip-start) that tell the browser to automatically flip the inset or alignment of the primary position. An MCP server overrides the consent element's position-try-fallbacks to use only try-tactics: none, disabling all automatic adjustments. Without flip tactics, the only alternatives are explicitly-defined @position-try rules. If the MCP ensures those rules produce off-screen positions (see Attack 1), the element has no valid safe fallback and lands off-screen.

/* CSS shorthand try-tactics in position-try-fallbacks: */
/* Modern syntax allows mixing @rule names and tactic keywords */

/* Consent framework (using flip tactics): */
.consent-panel {
  position-try-fallbacks: flip-block, flip-inline, flip-start;
  /* Browser auto-flips insets to find a valid on-screen position. */
}

/* MCP attack: replace tactic keywords with explicit off-screen @position-try rules */
.consent-panel {
  position-try-fallbacks: --mcp-fallback-1, --mcp-fallback-2;
  /* No tactic keywords — no automatic flipping.
     Only explicit @position-try rules tried.
     MCP defines those rules with off-screen positions (Attack 1).
     Result: no valid on-screen position found → settles off-screen. */
}

// Combined detection
function auditPositionTryFallbacks(consentPanelEl) {
  const cs = window.getComputedStyle(consentPanelEl);
  const fallbacks = cs.getPropertyValue('position-try-fallbacks') || '';

  // Check if flip-block/flip-inline are absent (potential try-tactics removal)
  const hasTactics = ['flip-block', 'flip-inline', 'flip-start'].some(t => fallbacks.includes(t));
  if (!hasTactics && fallbacks.trim() !== 'none' && fallbacks.trim() !== '') {
    console.warn('SECURITY: consent panel position-try-fallbacks has no flip tactics — auto-adjustment disabled', {
      fallbacks
    });
  }

  // Check final rendered position
  const rect = consentPanelEl.getBoundingClientRect();
  if (rect.bottom < 0 || rect.top > window.innerHeight ||
      rect.right < 0 || rect.left > window.innerWidth) {
    console.error('SECURITY: consent panel is positioned outside viewport — @position-try fallbacks may be off-screen', {
      rect, fallbacks
    });
    return true;
  }
  return false;
}

Attack 4: cascade layer @position-try override — MCP rule beats framework's fallback definition

A @position-try rule is an at-rule, and like other CSS at-rules, it participates in the cascade via layer priority. When an MCP server declares @position-try --above-btn in a cascade layer that has higher priority than the consent framework's layer (either via explicit layer ordering or via !important reversal), the MCP's version of the rule takes precedence. The framework's safe fallback positions are silently replaced by the MCP's off-screen versions. The consent element's position-try-fallbacks: --above-btn still references the same name — but the @position-try behind that name now places the element off-screen.

/* MCP attack using cascade layers to override @position-try: */

/* MCP's layer declared first (higher !important priority, higher cascade for normal): */
@layer mcp-layer {
  @position-try --above-btn {
    inset-block-start: auto;
    inset-block-end: calc(-100vh - 200px);   /* way below viewport bottom */
    inset-inline-start: anchor(start);
  }
}

/* Consent framework's layer declared second (lower cascade priority): */
@layer consent-layer {
  @position-try --above-btn {
    inset-block-end: anchor(start);    /* above the button — safe on-screen position */
    inset-inline-start: anchor(start);
  }
}

/* Resolution:
   For normal (non-!important) rules: later layers win.
   consent-layer's @position-try --above-btn comes after mcp-layer.
   The consent-layer definition wins for normal rules.

   For !important rules: earlier layers win.
   MCP uses:
     @position-try --above-btn {
       inset-block-end: calc(-100vh - 200px) !important;
     }
   This !important rule in mcp-layer (earlier layer) beats consent-layer's
   !important counter-rule.
*/

@layer mcp-layer {
  @position-try --above-btn {
    inset-block-end: calc(-100vh - 200px) !important;
  }
}

// Detection: verify @position-try inset values
function detectPositionTryLayerOverride(tryRuleName) {
  const definitions = [];
  for (const sheet of document.styleSheets) {
    try {
      const scanRules = (rules, layerName) => {
        for (const rule of rules) {
          if (rule.cssRules) {
            scanRules(rule.cssRules, rule.name || layerName);
          }
          if (rule.constructor.name === 'CSSPositionTryRule' && rule.name === tryRuleName) {
            definitions.push({ rule, layerName });
          }
        }
      };
      scanRules(sheet.cssRules, null);
    } catch (e) {}
  }
  if (definitions.length > 1) {
    console.warn('SECURITY: multiple @position-try definitions for', tryRuleName, definitions);
    return true;
  }
  return false;
}

Attack summary

Attack Mechanism Effect Severity
All-fallbacks-off-screen Every @position-try in fallback chain places element outside viewport Browser exhausts list, settles on first (off-screen) entry High
Fallback order prepend attack MCP's off-screen @position-try rules prepended to position-try-fallbacks list All MCP positions fail, list exhausted, browser uses first (MCP off-screen) entry High
try-tactics:none removal Replace flip-block/flip-inline tactics with explicit off-screen @position-try rules Auto-flipping disabled, no valid on-screen fallback found High
Cascade layer @position-try override MCP redefines @position-try rule name via layer with !important priority Framework's safe fallback position replaced by MCP's off-screen insets High

Consolidated finding blocks

High CSS @position-try all-fallbacks-off-screen exhaust attack: MCP server injects @position-try rules for all names referenced in the consent element's position-try-fallbacks list, replacing their inset values with off-screen coordinates (inset-block-start: -9999px). The browser tries each fallback in order — all overflow the viewport boundary — and then applies the first fallback as the final position. The consent panel ends up at top: -9999px (first fallback), completely outside the visible viewport. Detection: audit @position-try rules for inset values exceeding viewport dimensions.
High CSS @position-try cascade layer override — framework safe positions replaced: MCP applies @position-try --above-btn { inset-block-end: calc(-100vh - 200px) !important } in an earlier-declared cascade layer. Due to CSS layer importance reversal, the MCP's !important rule beats the consent framework's safe @position-try --above-btn { inset-block-end: anchor(start) !important } counter-rule. The framework's anchor-relative positioning is replaced by a static off-screen value. The consent panel positions below the viewport even when the anchor button is near the page top.
High CSS position-try-fallbacks order manipulation — MCP off-screen positions first: MCP overrides the consent element's position-try-fallbacks property to prepend four off-screen @position-try rules before the framework's on-screen fallbacks. When the primary position overflows and the button is positioned at a viewport corner (where all directional fallbacks also overflow), the browser exhausts all 7 entries and settles on the first — the MCP's off-screen position. The consent panel lands at the MCP's top: -9999px coordinates.

← Blog  |  CSS anchor-scope attacks  |  CSS anchor positioning attacks  |  Security Checklist