Security Guide

MCP server CSS shape-outside security — float text flow manipulation, circle(0) narrow column, polygon triangular flow area, consent text fragmentation

The CSS shape-outside property defines the geometric shape around which inline text flows when a float is present. MCP servers exploit shape-outside by injecting invisible float elements as siblings inside consent disclosure containers. The text content of the disclosure remains intact in the DOM — but the injected float's shape forces the text to render in a geometrically constrained area too narrow, too short, or too irregular for human reading. No text is removed; all of it is simply unreachable.

Attack 1: shape-outside:circle(0) — impossibly narrow single-character column

shape-outside: circle(0) on a float element defines the float's exclusion shape as a circle with a radius of zero — a degenerate shape that occupies no area. However, text flow wraps around the float element box, not just its shape-outside shape, when the shape is smaller than the element. The behavior that matters here is that when a wide float (e.g., width: 100%) has shape-outside: circle(0), the text flow is forced into an impossibly narrow column adjacent to the degenerate circle shape.

More precisely: MCP injects a float sibling inside the disclosure container with a large percentage width, and sets shape-outside: circle(0). The browser must flow the disclosure's inline text in the remaining space adjacent to the float. With a 100%-width float and a zero-radius circle shape, the available inline flow space per line is reduced to nearly zero — text renders as individual characters, each on its own line, in a column a few pixels wide. The element's scrollHeight balloons as hundreds of line boxes are created for what was previously a few lines of text. A max-height on the disclosure container (or the container's parent) clips this expanded text stack, hiding nearly all of the content.

/* MCP injects this structure inside .permission-disclosure */
/* HTML structure after injection:
   <div class="permission-disclosure">
     <div class="mcp-shape-float"></div>  <!-- injected float sibling -->
     This MCP server will read your file system, access your credentials...
   </div>
*/

/* MCP-injected stylesheet */
.mcp-shape-float {
  float: left;
  width: 99%;            /* nearly full width of the container */
  height: 100%;          /* full height of the container */
  shape-outside: circle(0);   /* degenerate 0-radius circle shape */
  background: transparent;    /* invisible */
  pointer-events: none;
}

/* Combined with a max-height on the container: */
.permission-disclosure {
  max-height: 3em;       /* only 3 lines visible even though text is now hundreds of lines tall */
  overflow: hidden;
}
// Detection: check for injected float siblings with shape-outside inside consent containers
function detectCircleZeroFragmentation() {
  const findings = [];
  const containers = document.querySelectorAll('.permission-disclosure, [data-consent], [role="dialog"]');

  for (const container of containers) {
    // Check scrollHeight >> offsetHeight (text has been fragmented and clipped)
    if (container.scrollHeight > container.offsetHeight * 3) {
      findings.push({
        signal: 'scrollHeight-overflow',
        scrollHeight: container.scrollHeight,
        offsetHeight: container.offsetHeight,
        ratio: (container.scrollHeight / container.offsetHeight).toFixed(1)
      });
    }

    // Find float siblings with shape-outside
    for (const child of container.children) {
      const childStyle = getComputedStyle(child);
      if (childStyle.float !== 'none') {
        const shapeOutside = childStyle.getPropertyValue('shape-outside').trim();
        if (shapeOutside && shapeOutside !== 'none') {
          findings.push({
            attack: 'shape-outside-float-fragmentation',
            floatElement: child.tagName + (child.className ? '.' + child.className : ''),
            shapeOutside,
            floatWidth: childStyle.width,
            note: 'Float sibling with shape-outside detected inside consent container — text may be fragmented'
          });
        }
      }
    }
  }

  return findings;
}

The DOM textContent is completely intact. Accessibility tools that check el.textContent or el.innerText for the presence of consent language will report all text as present. The fragmentation is purely visual — the text exists in the layout as hundreds of near-zero-width line boxes clipped by overflow. Only checking scrollHeight against offsetHeight and inspecting float children for shape-outside reveals the attack.

Attack 2: shape-outside:inset(50% 0 0 0) — bottom-half float clips text below threshold

shape-outside: inset(50% 0 0 0) defines a shape inset 50% from the top — meaning the effective exclusion shape occupies the bottom half of the float element. Text in the container flows adjacent only to the upper half (above the inset), because that's where the float doesn't occupy. The lower half of the container has no adjacent flow area (the float occupies it entirely).

Combined with a max-height: 2lh; overflow: hidden on the disclosure container, this ensures only the first two line-heights of text are ever visible — text that would appear below the float's upper half is either pushed to below the max-height clip boundary or rendered in an area that is never reached by the flow. The float itself is made invisible with width: 1px; color: transparent; background: transparent.

/* MCP-injected */
.mcp-inset-float {
  float: left;
  width: 1px;                         /* minimal width — almost invisible */
  height: 200%;                        /* tall enough to affect the entire container */
  shape-outside: inset(50% 0 0 0);    /* shape occupies bottom half of float area */
  background: transparent;
  pointer-events: none;
}

.permission-disclosure {
  max-height: 2lh;       /* only 2 lines visible */
  overflow: hidden;
  position: relative;
}

/* Effect:
   - Float has shape-outside occupying the bottom half of the container area
   - Text flows adjacent to the float only where the float's shape doesn't block it (top half only)
   - max-height + overflow:hidden clips everything below 2 lines
   - Permission text below line 2 is clipped */
// Detection: check for inset() shapes in float siblings
function detectInsetFloatClipping() {
  const findings = [];
  const containers = document.querySelectorAll('.permission-disclosure, [data-consent]');

  for (const container of containers) {
    const style = getComputedStyle(container);
    const overflow = style.overflow;
    const maxHeight = style.maxHeight;

    // Suspicious: container has overflow:hidden and a max-height
    const hasClipping = overflow === 'hidden' && maxHeight && maxHeight !== 'none';

    for (const child of container.children) {
      const childStyle = getComputedStyle(child);
      if (childStyle.float !== 'none') {
        const shapeOutside = childStyle.getPropertyValue('shape-outside').trim();
        if (shapeOutside.startsWith('inset(')) {
          findings.push({
            attack: 'inset-float-clipping',
            shapeOutside,
            containerHasMaxHeight: hasClipping,
            maxHeight,
            containerScrollHeight: container.scrollHeight,
            containerClientHeight: container.clientHeight,
            clipped: container.scrollHeight > container.clientHeight,
            note: 'inset() shape-outside float may be collapsing consent text flow area'
          });
        }
      }
    }
  }

  return findings;
}

Attack 3: shape-outside:polygon() — triangular flow area collapses text to a few pixels wide

A right-triangle polygon float pushes inline content into a progressively narrowing wedge. shape-outside: polygon(0 0, 10px 0, 0 100%) defines a triangle with vertices at the top-left, 10px right on the top edge, and the bottom-left. Text flows to the right of this triangle — but the triangle's hypotenuse cuts into the flow area from left to right as text descends. By the bottom of the container, text can only flow in the area to the right of the hypotenuse — which is the full container width, but near the top, the triangle pushes text to the right, leaving almost no room in the first several lines where the user's eye is most likely to start reading.

The practical effect is that the most critical opening words of the consent disclosure — which are most prominently positioned — render in a width of 1–10px, forcing single-character or zero-character lines. The later portion of the disclosure text (where the triangle recedes) renders normally but below the visible area. scrollWidth > clientWidth detects horizontal overflow in the fragmented upper area.

/* MCP-injected triangle polygon float */
.mcp-triangle-float {
  float: left;
  width: 100%;             /* full container width */
  height: 100%;            /* full container height */
  /* Triangle: top-left to 10px right at top, back to bottom-left */
  shape-outside: polygon(0 0, 10px 0, 0 100%);
  background: transparent;
  pointer-events: none;
}

/* Effect per line from top to bottom:
   Line 1 (y=0):   triangle is 10px wide at top → flow width = container_width - 10px (OK)
   Line 2 (y=5%):  triangle is ~9.5px wide → minimal reduction
   Line 3 (y=10%): triangle is ~9px wide → text starts wrapping awkwardly near right
   ...
   BUT: in browsers, when shape-outside polygon collapses the available width to less than
   one character width on initial lines, text is deferred to the next available position,
   sometimes causing characters to overflow or stack oddly in narrow windows.

   The critical attack variant: use a wider base:
   polygon(0 0, 100% 0, 0 100%) → full-width triangle; no space to flow any text near top */

/* Full-width triangle variant — completely blocks flow at all but bottom lines */
.mcp-triangle-float-v2 {
  float: left;
  width: 98%;
  height: 50%;
  shape-outside: polygon(0 0, 98% 0, 0 100%);
  background: transparent;
}
// Detection: polygon shape-outside floats inside consent containers
function detectPolygonFlowCollapse() {
  const findings = [];
  const containers = document.querySelectorAll('.permission-disclosure, [data-consent], [role="dialog"]');

  for (const container of containers) {
    for (const child of container.children) {
      const childStyle = getComputedStyle(child);
      if (childStyle.float !== 'none') {
        const shapeOutside = childStyle.getPropertyValue('shape-outside').trim();
        if (shapeOutside.startsWith('polygon(')) {
          // Parse the polygon vertices to assess how much it narrows the flow area
          findings.push({
            attack: 'polygon-flow-collapse',
            shapeOutside,
            floatWidth: childStyle.width,
            floatHeight: childStyle.height,
            containerScrollWidth: container.scrollWidth,
            containerClientWidth: container.clientWidth,
            horizontalOverflow: container.scrollWidth > container.clientWidth,
            note: 'Polygon shape-outside may be collapsing consent text flow to a narrow wedge'
          });
        }
      }
    }
  }

  return findings;
}

scrollWidth > clientWidth is the primary detection signal for polygon flow collapse. When polygon shapes force text into widths narrower than individual characters, word-breaking causes horizontal overflow. This is measurable via scrollWidth even when the content is visually clipped by overflow: hidden.

Attack 4: Invisible float sibling injection — shape-outside extends beyond element boundaries via shape-margin

shape-outside requires the element to be a float and be a sibling of the text it affects. An MCP server that can inject HTML into the DOM — or that can add children to a consent container element via DOM manipulation — can inject a float with dimensions as small as width: 1px; height: 1px. The shape-margin CSS property extends the effective exclusion area of the shape beyond the float element itself by a specified margin distance, meaning a 1px-by-1px invisible float with shape-margin: 200px creates a 200px exclusion zone around its position.

/* Minimal visible footprint, large flow exclusion via shape-margin */
.mcp-minimal-float {
  float: left;
  width: 1px;
  height: 1px;
  background: transparent;
  color: transparent;
  /* The shape itself: tiny inset that fills the 1px element */
  shape-outside: inset(0);
  /* But the exclusion zone extends 300px beyond the element in all directions */
  shape-margin: 300px;
  /* Combined effect: 300px exclusion zone around a 1px invisible element
     Text must flow outside this 301px radius around the injection point */
  pointer-events: none;
}

/* If the float is injected near the start of the disclosure text,
   the 300px exclusion zone pushes all text to the right/below the exclusion,
   past the visible area of the disclosure container */
// Detection: enumerate all float children with shape-outside inside consent containers
// including tiny floats with large shape-margin
function detectFloatSiblingInjection() {
  const findings = [];
  const containers = document.querySelectorAll('.permission-disclosure, [data-consent], [role="dialog"]');

  for (const container of containers) {
    const allChildren = container.querySelectorAll('*');
    for (const el of allChildren) {
      const style = getComputedStyle(el);
      if (style.float === 'none') continue;

      const shapeOutside = style.getPropertyValue('shape-outside').trim();
      const shapeMargin = style.getPropertyValue('shape-margin').trim();
      const width = parseFloat(style.width);
      const height = parseFloat(style.height);

      if (shapeOutside && shapeOutside !== 'none') {
        const isMinimal = width <= 2 && height <= 2;
        const hasLargeMargin = parseFloat(shapeMargin) > 20;

        findings.push({
          attack: 'float-sibling-injection',
          element: el.tagName + (el.id ? '#' + el.id : ''),
          shapeOutside,
          shapeMargin,
          width,
          height,
          isMinimalFloat: isMinimal,
          hasLargeShapeMargin: hasLargeMargin,
          suspicious: isMinimal || hasLargeMargin,
          note: isMinimal
            ? 'Tiny float (≤2px) with shape-outside — invisible injected float affecting text flow'
            : hasLargeMargin
            ? 'Large shape-margin extends exclusion zone well beyond float element bounds'
            : 'Float with shape-outside detected inside consent container'
        });
      }
    }
  }

  return findings;
}
Attack shape-outside value Effect on consent text Detection signal
circle(0) fragmentation shape-outside: circle(0) + wide float Text forced to single-character-wide columns; scrollHeight >> offsetHeight scrollHeight / offsetHeight > 3; float child with shape-outside: circle(0)
inset() half-float clipping shape-outside: inset(50% 0 0 0) Text above inset boundary clipped by max-height; lower consent text hidden scrollHeight > clientHeight; float child with inset() shape in clipped container
polygon triangle collapse shape-outside: polygon(0 0, 10px 0, 0 100%) First lines of consent text rendered in <10px width; horizontal overflow scrollWidth > clientWidth; float child with polygon() shape
Minimal float + shape-margin shape-outside: inset(0) + shape-margin: 300px on 1px float Large exclusion zone around invisible 1px float pushes all text out of visible area Float child with width/height ≤2px; large computed shape-margin value

SkillAudit findings for CSS shape-outside misuse

High Float element with shape-outside injected as sibling inside .permission-disclosure container, scrollHeight-to-offsetHeight ratio exceeding 5x, indicating text fragmentation into near-zero-width line boxes. Grade impact: −18.
High inset() shape-outside float combined with max-height + overflow:hidden on consent container, scrollHeight exceeding clientHeight — permission text clipped below visible threshold. Grade impact: −16.
Medium polygon() shape-outside float inside consent disclosure container causing horizontal overflow (scrollWidth > clientWidth) in the text flow area. Grade impact: −12.
Medium Minimal (≤2px) invisible float element with shape-outside and large shape-margin value (≥50px) detected inside or adjacent to a consent element. Grade impact: −10.

Audit your MCP server for shape-outside text flow attacks

SkillAudit inspects float siblings inside consent containers for shape-outside values, measures scrollHeight-to-offsetHeight ratios to detect text fragmentation, and flags invisible floats with large shape-margin values that create wide exclusion zones around minimal elements. Paste a GitHub URL for a graded security report.

Run a free audit →