MCP Security Reference

MCP server CSS display:flow-root security

display: flow-root creates a Block Formatting Context (BFC) without clipping overflow — unlike overflow: hidden, elements inside the BFC can still visually overflow the container. MCP servers exploit two BFC behaviors that flow-root enables: (1) margin collapse suppression, which forces consent paragraph margins to add rather than merge with the parent, pushing consent below the visible fold; (2) float containment within the BFC, where a floated sibling occupies the same visual row as consent text and overlaps it. Detectors that flag only overflow: hidden as a BFC trigger miss display: flow-root entirely.

Attack findings

HIGHSA-CSS-DFRT-001 — display:flow-root on dialog container prevents margin collapse; consent paragraph margin-top adds to parent padding instead of merging; consent pushed below visible fold inside fixed-height dialog
HIGHSA-CSS-DFRT-002 — display:flow-root + float:left sibling (width:100%, height:100%) within BFC; float occupies full dialog area; consent text flows around float and is pushed into overflow area
HIGHSA-CSS-DFRT-003 — display:flow-root + negative margin-bottom on float sibling + consent follows float; negative margin pulls float up, compressing consent vertical position; consent overlaps dialog header and is clipped by dialog max-height
MEDIUMSA-CSS-DFRT-004 — JS mousedown sets display:flow-root on dialog wrapper + injects float sibling; consent text position changes in click frame; static analysis finds no BFC or float in initial state

Background: display:flow-root and BFC properties

display: flow-root is a CSS Display Level 3 value that explicitly creates a BFC as its primary purpose, unlike overflow: hidden or float which create BFCs as side effects. A BFC has four key behaviors that distinguish it from normal block layout:

Detection gap: Common BFC checks look for overflow: hidden/auto/scroll or float: left/right on the container. display: flow-root creates a BFC without either property being present. A detector scanning for BFC triggers via the overflow or float properties will report clean — the display property must also be checked.

Attack 1 — margin collapse suppression pushes consent below fold (SA-CSS-DFRT-001)

In normal block layout, a child element's margin-top collapses with its parent's margin-top when there is no border, padding, or inline content between them. This means a consent paragraph with margin-top: 200px inside a container with zero padding does not necessarily push 200px of space inside the container — the margin may collapse with the parent, producing only the larger of the two margins. When the parent is a display: flow-root BFC, margin collapse is suppressed: the 200px margin-top is added inside the BFC, after the BFC's own padding. If the dialog has a fixed height of 300px, the consent paragraph now starts 200px from the top of the content area — already past the visible fold if the dialog shows only ~100px of content area.

/* Attack: BFC margin-collapse suppression forces consent below visible area */
.install-dialog {
  display: flow-root;   /* creates BFC — no margin collapse with children */
  height: 300px;
  overflow: hidden;     /* clip at 300px */
  /* install button is direct child, no margin-top */
}
.consent-text {
  margin-top: 250px;   /* in normal flow this might collapse; in BFC it does not */
  /* consent starts 250px from top of dialog content area */
  /* dialog height is 300px — only 50px of consent visible at bottom */
  /* key clauses in the middle of the consent block are below the fold */
}

SA-CSS-DFRT-001 (High). The consent element is in-layout and in-DOM. Its getBoundingClientRect() is partially in-viewport (the bottom 50px). The key clauses are out-of-viewport due to the margin. Detection: check getComputedStyle(el).display === 'flow-root' on the consent's parent; check consent child marginTop; compare consent BCR top against dialog BCR top — a margin-top pushing the consent start more than 20% of dialog height below the fold is suspicious.

/* Detection */
function checkFlowRootMarginAttack(consentEl) {
  const parent = consentEl.parentElement;
  if (!parent) return null;
  const parentCs = getComputedStyle(parent);
  if (parentCs.display !== 'flow-root') return null;
  const cs = getComputedStyle(consentEl);
  const marginTop = parseFloat(cs.marginTop) || 0;
  const parentHeight = parent.getBoundingClientRect().height;
  // flag if consent margin-top is more than 30% of parent height
  if (marginTop > parentHeight * 0.3) {
    const consentTop = consentEl.getBoundingClientRect().top;
    const parentBottom = parent.getBoundingClientRect().bottom;
    if (consentTop > parentBottom - 60) {
      return { vuln: 'SA-CSS-DFRT-001',
        detail: `parent display:flow-root, consent marginTop:${marginTop}px, mostly below fold` };
    }
  }
  return null;
}

Attack 2 — full-width float sibling occupies dialog, consent pushed to overflow (SA-CSS-DFRT-002)

Within a display: flow-root BFC, block children flow around floated siblings. An MCP server inserts a floated <div> that is 100% of the dialog's width and height — effectively a full-size cover element. Because the BFC contains its floats, this float does not escape the dialog box. Because block children of the BFC flow around the float (avoiding it), the consent paragraph is pushed to below the float's bottom edge. The float's height equals the dialog's height. The consent, flowing below the float, starts below the dialog's visible area. The dialog shows the float's content (perhaps a decorative image or the install button). The consent is in the DOM, has correct dimensions, but is positioned below the dialog fold.

/* Attack: full-width float sibling in flow-root BFC pushes consent below fold */
.install-dialog {
  display: flow-root;
  height: 400px;
  overflow: hidden;
}
.install-dialog::before {
  content: '';
  float: left;
  width: 100%;
  height: 400px; /* full dialog height */
  /* shows install button as background image */
  background: white url('/install-btn.svg') no-repeat center;
}
.consent-text {
  /* flows below the 400px float in the BFC */
  /* starts at 400px from top of dialog — outside the 400px dialog height */
  display: block; visibility: visible; opacity: 1; /* all checks pass */
}

Attack 3 — negative margin-bottom on float compresses consent position (SA-CSS-DFRT-003)

A float with a large positive height followed by a negative margin-bottom on that float adjusts the text flow offset. In a BFC, the consent paragraph flows below the float bottom minus the negative margin. This allows fine-grained control of exactly where the consent text ends up — not necessarily entirely off-screen, but with key clauses pushed just past the dialog's visible fold. A negative margin-bottom of -150px on a 300px tall float places the consent start at 150px from the top of the dialog — in a 200px dialog, the consent is partially visible (the header line is visible, key permission clauses below 200px are not).

/* Attack: negative margin-bottom on float fine-tunes consent position */
.install-dialog {
  display: flow-root;
  height: 200px; /* visible dialog height */
  overflow: hidden;
}
.install-dialog::before {
  content: '';
  float: left;
  width: 100%;
  height: 300px;
  margin-bottom: -150px; /* consent starts at 300-150 = 150px from top */
}
.consent-text {
  /* starts at 150px — "By installing you agree..." header visible at top */
  /* key clause "...grant shell execution..." starts at ~230px — outside 200px dialog */
}

Attack 4 — JS mousedown flow-root + float injection (SA-CSS-DFRT-004)

At page load, the dialog has display: block (no BFC) and no floats. Consent is fully visible. At mousedown on the install button, JS changes the dialog's display to flow-root and inserts a full-height floated pseudo-content div before the consent element. The consent immediately flows below the float in the click frame. Static analysis at page load finds a normal block dialog with no BFC and no floats — completely clean. The attack fires in the single frame between mousedown and click.

/* Attack: runtime flow-root + float injection at mousedown */
installBtn.addEventListener('mousedown', () => {
  dialog.style.display = 'flow-root';
  dialog.style.height = '400px';
  dialog.style.overflow = 'hidden';
  const blocker = document.createElement('div');
  blocker.style.cssText = 'float:left;width:100%;height:400px;background:white';
  dialog.insertBefore(blocker, consentEl);
});

/* Detection: MutationObserver on dialog element */
new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (m.type === 'childList' && m.addedNodes.length > 0) {
      for (const node of m.addedNodes) {
        if (node.nodeType === 1 && getComputedStyle(node).float !== 'none') {
          flagTampering('SA-CSS-DFRT-004');
          installBtn.disabled = true;
        }
      }
    }
    if (m.type === 'attributes') {
      if (getComputedStyle(dialog).display === 'flow-root') {
        flagTampering('SA-CSS-DFRT-004');
        installBtn.disabled = true;
      }
    }
  }
}).observe(dialog, { attributes: true, attributeFilter: ['style'], childList: true });

SkillAudit detection: SkillAudit checks display:flow-root on consent ancestors and audits BFC children for float siblings and large margin-top values on the consent element. It also monitors for runtime flow-root or float injection via MutationObserver during the simulated install click. Run a free audit →

Detection summary

Attack IDProperties involvedKey detection signal
SA-CSS-DFRT-001parent display:flow-root + consent margin-top > 30% dialog height + dialog overflow:hiddenparent.display === 'flow-root' AND consentEl marginTop > parentHeight * 0.3
SA-CSS-DFRT-002parent display:flow-root + float::before sibling 100% width × full height + consent pushed below foldfloat sibling with width:100% and height ≥ dialog height before consent element
SA-CSS-DFRT-003parent display:flow-root + float sibling with negative margin-bottom + consent partially below foldfloat sibling negative marginBottom + consent BCR top > dialog BCR bottom - threshold
SA-CSS-DFRT-004JS mousedown sets dialog display:flow-root + inserts floated div before consentMutationObserver on dialog: display attribute change + childList floated insertion