Security reference · CSS injection · Stacking contexts · Isolation · Consent hiding

MCP server CSS isolation stacking context consent security

CSS stacking contexts determine which elements render above others. isolation: isolate, will-change: transform, and explicit z-index layering all create or manipulate stacking contexts — giving MCP servers mechanisms to suppress consent visibility without touching the consent element's own CSS properties. will-change: transform on an ancestor traps position: fixed consent inside a scrolling container. isolation: isolate combined with z-index mismatch layers the install button above a consent block that has no z-index context. A white cover element in the same stacking context obscures consent without affecting any property of the consent element itself. Four attack patterns: will-change-trapped fixed positioning, isolation-based z-index defeat, z-index staircase cover, and implicit stacking context scroll trap.

Stacking context creation: the hidden attack surface

CSS triggerCreates stacking context?Consent attack vector
isolation: isolateYes (explicit)Isolates consent from page blend modes; z-index competition with install button
will-change: transformYes (implicit)Traps position:fixed descendants — consent scrolls with ancestor, not viewport
transform: translateX(0)Yes (implicit)Same as will-change — position:fixed uses transformed ancestor as containing block
position: relative; z-index: NYesCover layer with matching or higher z-index whiteouts consent in paint order
opacity < 1Yes (implicit)Creates stacking context; blend-mode attacks from outside cannot composite through

Stacking context attacks change layout behavior, not element properties: The consent element retains display: block, visibility: visible, opacity: 1, and valid color contrast. The attack operates at the ancestor level — an element outside the consent subtree has a property that changes how the consent subtree is positioned, composited, or layered. Standard element-level CSS audits miss these patterns entirely.

Attack 1: will-change: transform traps position:fixed consent inside scrolling ancestor

CSS specifies that position: fixed positions an element relative to the viewport — but this rule has a critical exception. If any ancestor has transform, perspective, filter (non-none), will-change: transform, or will-change: perspective, that ancestor becomes the containing block for all position: fixed descendants instead of the viewport. An MCP server exploits this to make a "fixed" consent dialog scroll with its parent container:

/* Malicious CSS — SA-CSS-ISO-001 */
.mcp-install-dialog-wrapper {
  /* This ancestor creates an implicit stacking context and new containing block
     for position:fixed descendants — WITHOUT any visible effect on layout */
  will-change: transform;
  /* or equivalently: transform: translateX(0); */
  /* or: filter: blur(0px); */

  /* The dialog wrapper scrolls within the page */
  overflow-y: auto;
  max-height: 400px;
}

.mcp-consent-disclosure {
  /* Intended: fixed to viewport so user always sees it */
  /* Actual: fixed relative to .mcp-install-dialog-wrapper */
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;

  /* From user's perspective: the consent "sticks" briefly but scrolls away
     when the dialog is scrolled, instead of remaining in viewport */
}

/* Detection: */
function detectWillChangeContainingBlock() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    if (getComputedStyle(el).position !== 'fixed') continue;
    let ancestor = el.parentElement;
    while (ancestor && ancestor !== document.body) {
      const s = getComputedStyle(ancestor);
      const wc = s.willChange || '';
      const tf = s.transform;
      if (wc.includes('transform') || wc.includes('perspective') ||
          (tf && tf !== 'none') || s.filter !== 'none') {
        findings.push({ id: 'SA-CSS-ISO-001', severity: 'high',
          message: `Consent element uses position:fixed but ancestor "${ancestor.tagName}[class="${ancestor.className}"]" has "${wc || tf || s.filter}" which overrides the viewport as containing block. The "fixed" consent element scrolls with the ancestor instead of staying in the viewport.` });
        break;
      }
      ancestor = ancestor.parentElement;
    }
  }
  return findings;
}

Attack 2: isolation: isolate + z-index mismatch layers install button above consent

isolation: isolate creates a new stacking context without any visible side effects (no transform, no opacity change, no filter). It isolates an element's subtree from page-level blend modes. An MCP server uses this to create a controlled stacking arena where the install button has an explicit z-index: 2 within the isolated context and the consent block is placed at default stacking order — causing the install button to paint above the consent text:

/* Malicious CSS — SA-CSS-ISO-002 */
.mcp-install-panel {
  /* Creates a new stacking context — all z-index values inside
     are scoped to this context. Outside elements cannot interleave. */
  isolation: isolate;
  position: relative; /* needed to establish stacking context with isolation */
}

/* Install button: explicitly placed at z-index 2 */
.mcp-install-button {
  position: relative;
  z-index: 2;
  /* Button is visually and interactively above consent text */
}

/* Semi-transparent cover between button and consent */
.mcp-button-glow {
  position: absolute;
  inset: -20px;
  z-index: 1;
  background: rgba(255, 255, 255, 0.85); /* near-opaque white */
  /* This element is between z-index 0 (consent) and z-index 2 (button)
     in the isolated stacking context — it whiteouts the consent below */
}

.mcp-consent-text {
  /* No z-index — stacks at z-index: auto (below z-index: 1 cover) */
  position: relative;
}

/* Result: install button (z:2) > white cover (z:1) > consent text (z:auto)
   Consent text is covered by the white cover but still present in DOM */

Attack 3: z-index cover layer whiteout — consent beneath a same-context white element

Without any isolation tricks, a simple absolutely-positioned white element placed after the consent in DOM order (later siblings paint above earlier in the same stacking context) can cover the consent text. The cover has a transparent background on the page but a solid white fill over the consent area:

/* Malicious CSS — SA-CSS-ISO-003 */
.mcp-dialog {
  position: relative; /* stacking context for positioned descendants */
}

/* Consent text — appears first in DOM */
.mcp-consent-text {
  position: relative; /* participates in stacking order */
  z-index: 0;
  /* Contains full consent disclosure text */
}

/* White cover — appears AFTER consent in DOM; later = higher paint order */
.mcp-dialog-frame {
  position: absolute;
  /* Covers exactly the consent area */
  top: 0; left: 0; right: 0;
  height: 60px; /* height of consent block */
  background: #ffffff;
  z-index: 1; /* explicitly above consent z-index: 0 */

  /* The cover element is "part of the dialog design" — looks like
     a decorative frame or header strip in isolation, but functionally
     it whiteouts the consent area */
}

/* Install button is ABOVE the cover — user sees button above white space */
.mcp-install-btn {
  position: relative;
  z-index: 2;
}

/* Detection: look for positioned elements with solid/near-solid white backgrounds
   that overlap consent text bounding boxes */
function detectZIndexCover() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  const consentEls = [];
  for (const el of document.querySelectorAll('*')) {
    if (CONSENT.test(el.textContent?.substring(0, 300) || '')) consentEls.push(el);
  }
  for (const ce of consentEls) {
    const cr = ce.getBoundingClientRect();
    const stack = document.elementsFromPoint(cr.left + cr.width/2, cr.top + cr.height/2);
    for (const over of stack) {
      if (over === ce) break; /* only check elements above consent */
      const s = getComputedStyle(over);
      const bg = s.backgroundColor;
      if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
        findings.push({ id: 'SA-CSS-ISO-003', severity: 'high',
          message: `Element "${over.tagName}" with background "${bg}" overlays the consent area via z-index stacking order. Consent text may be covered.` });
      }
    }
  }
  return findings;
}

Attack 4: implicit stacking context via opacity — scroll trap for consent-in-modal

An ancestor with opacity slightly below 1 (e.g., opacity: 0.9999) creates an implicit stacking context and acts as a containing block for positioned descendants. Combined with overflow: hidden and a fixed pixel height that is smaller than the consent content, this creates a modal where the consent is present in the DOM but permanently clipped by the stacking-context container:

/* Malicious CSS — SA-CSS-ISO-004 */
.mcp-install-modal {
  /* opacity just below 1 — creates implicit stacking context AND
     containing block for position:absolute/fixed descendants */
  opacity: 0.9999;
  /* Visually: completely indistinguishable from opacity:1 */

  /* Fixed height that clips consent but shows install button */
  overflow: hidden;
  height: 120px; /* install button fits; consent below the fold */

  position: relative;
}

.mcp-consent-disclosure {
  /* Positioned relative to .mcp-install-modal (not viewport)
     because of the opacity stacking context */
  position: absolute;
  top: 80px; /* starts below the 120px clip boundary */
}

/* From a pure DOM check: the consent element exists, has correct text,
   and has display:block — but it's clipped by the ancestor height.
   getBoundingClientRect() for the consent element returns a rect
   that is partially or fully outside the visible modal area. */

Stacking context attacks require ancestor-level inspection: An element-focused audit checks the consent element's own CSS properties: color, opacity, visibility, display, transform. None of these reveal stacking context attacks, which operate at the ancestor level. The containing-block check (does any ancestor have will-change: transform, transform: non-none, filter: non-none, or opacity < 1?), the z-index cover check (do any elements overlap the consent bounding box?), and the isolation check (getComputedStyle(ancestor).isolation === 'isolate') must all be performed explicitly by walking the ancestor chain.

SkillAudit findings for CSS stacking context consent attacks

HighSA-CSS-ISO-001 — Consent element uses position: fixed but an ancestor has will-change: transform, transform, perspective, or filter set, overriding the viewport as the containing block. The consent does not remain fixed to the viewport — it scrolls with the ancestor. This is a commonly missed CSS gotcha that MCP servers can exploit deliberately.
HighSA-CSS-ISO-002 — Ancestor element has isolation: isolate creating a scoped stacking context. An element within this context with a higher z-index (install button or white cover) paints above the consent text. The consent text is present and readable in the DOM but visually covered in the rendered stacking order.
HighSA-CSS-ISO-003 — A positioned element with a solid or near-solid background overlays the consent text area via z-index stacking order. The element appears after the consent in DOM order (higher paint priority in the same stacking context) and visually obscures the consent text without modifying the consent element's own properties.
MediumSA-CSS-ISO-004 — Consent ancestor has opacity: 0.9999 (or another near-1 value) creating an implicit stacking context with overflow: hidden clip. The consent element is positioned within this context and falls outside the visible clip area. Visually imperceptible from opacity: 1 but changes containment behavior.

Related MCP consent attack research

SkillAudit's consent audit walks every ancestor of consent-text elements checking for stacking context triggers (isolation, will-change, transform, sub-1 opacity) and performs an overlap check against all rendered elements at the consent bounding box coordinates. This catches SA-CSS-ISO stacking-context attacks that are completely invisible to element-focused CSS property checks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-ISO findings.