Security reference · CSS injection · shape-outside · Consent manipulation

MCP server CSS shape-outside consent security — float shape wrapping consent displacement

CSS shape-outside redefines the geometric boundary around which inline content wraps when it flows around a floated element. An attacker places a transparent floated sibling adjacent to consent text and applies a shape-outside geometry that collapses the available wrapping area. The consent text is forced into an invisible column, pushed outside the overflow-clipped container, or displaced entirely. The float element itself has no visual appearance — float auditors see a zero-size transparent float and conclude it is harmless, missing the wrapping-area attack entirely.

shape-outside attack surface overview

Attack ID Shape function Key mechanism Audit blind spot
SA-CSS-SHP-001 polygon(...) on adjacent float Complex polygon reduces wrapping area to a 1px-wide column at the left edge — each line of consent text wraps to 1–2 characters wide Auditor checks float element dimensions and visibility — float is 0×0 or transparent; wrapping geometry not evaluated
SA-CSS-SHP-002 circle(0%) on full-width float Full-width (100%) float with circle(0%) shape has a zero-radius exclusion circle — all inline content is displaced below the float, outside the overflow:hidden container boundary Auditor sees float:left + overflow:hidden; does not check shape-outside value; content displacement appears to be a normal overflow clip
SA-CSS-SHP-003 inset(0px) on oversized float Float wider than the container with inset(0px) shape creates an exclusion zone wider than the container — consent text overflows into a zero-width wrapping area, all lines stacked at 0px width Auditor checks overflow:hidden on container — consent text is in DOM but flows into zero-width area; overflow clips it
SA-CSS-SHP-004 shape-margin extends exclusion zone shape-margin: 200px on a consent-adjacent float extends the non-wrapping margin around the shape geometry, pushing consent lines outside the visible container without changing the float's visual footprint Auditor checks float dimensions and margin — shape-margin is not the same as CSS margin; float dimensions look normal

Float masquerade: CSS shape-outside only has effect on floated elements. An attacker's shape-outside element must have float: left or float: right. A floated element with no visible dimensions (width: 0; height: 0) or transparent background is common in CSS layout patterns used for clearfix, column gutters, and flow control. The attack element blends in with these patterns — code reviewers see a small or zero-size float and do not investigate the shape-outside value further.

Background: how CSS shape-outside works

CSS shape-outside is part of the CSS Shapes specification. When applied to a floated element, it defines the geometric boundary around which inline content (text, inline-blocks) in the same formatting context will wrap. Without shape-outside, inline content wraps around the rectangular margin box of the float. With shape-outside, inline content wraps around the specified shape. The shape is clipped to the float's margin box — content wraps around the outer edge of the defined shape, not the float's rectangular box. The available wrapping area is the container width minus the exclusion area of the shape. If the shape consumes most or all of the container width, the available wrapping area for consent text approaches zero.

Attack 1: polygon shape compresses consent wrapping area to 1px column (SA-CSS-SHP-001)

A polygon defined with shape-outside: polygon(0% 0%, 99% 0%, 99% 100%, 0% 100%) on a float occupying the full height of the consent container creates an exclusion area that covers 99% of the container width. Consent text must wrap around this shape, limiting its wrapping area to the remaining 1% of the container — typically 2–5 pixels wide. Each line of the 14px consent text wraps into a 1–5 pixel column, stacking characters vertically and making the text effectively illegible. The float has no visual appearance if its background is transparent and it is positioned outside the visible flow, but its shape-outside geometry still controls where the sibling consent text can flow.

/* Attack: polygon shape-outside reduces consent wrapping area to ~1px */

/* HTML structure:
   <div class="consent-container">
     <div class="shape-float"></div>  <!-- shape float must precede consent text -->
     <p class="consent-text">By installing this server you authorize...</p>
   </div>
*/

.consent-container {
  width: 400px;
  overflow: hidden;  /* clips consent text that overflows the container */
}

.shape-float {
  float: left;
  width: 400px;     /* same width as container */
  height: 200px;    /* tall enough to cover all consent text lines */
  background: transparent; /* invisible */
  shape-outside: polygon(
    0% 0%,
    99% 0%,    /* shape covers 99% of the container width */
    99% 100%,
    0% 100%
  );
  /* The inline wrapping area is: container_width - shape_right_edge
     = 400px - (99% × 400px) = 400px - 396px = 4px
     Each line of consent text has only 4px of wrapping width.
     At font-size:14px, 4px is less than one character width.
     The consent text wraps at sub-character widths — all text is clipped.

     Auditor seeing the float:
     - background: transparent ✓ (no visual)
     - width: 400px — same as container (not unusual for a layout float)
     - height: 200px — tall float for layout purposes
     - shape-outside: polygon(...) — not checked

     Auditor finds: transparent float with shape-outside. Does not evaluate
     the geometric effect on sibling text wrapping. PASSES. */
}

/* Variant: irregular polygon that provides 1px column on left */
.shape-float-v2 {
  float: right;
  width: 50%;
  height: 200px;
  background: transparent;
  shape-outside: polygon(
    1px 0px,    /* left edge is 1px from float's left side */
    100% 0px,   /* top-right corner */
    100% 100%,  /* bottom-right corner */
    1px 100%    /* back to left edge at 1px */
  );
  /* Wrapping area on the right of the shape: 1px */
  /* Wrapping area on the left: the non-floated left side */
  /* Combined with float:right, consent wraps in a 1px column on the right */
}

/* Variant: stepped polygon that alternates between 0px and 400px column widths */
.shape-float-v3 {
  float: left;
  width: 400px;
  height: 400px;
  background: transparent;
  shape-outside: polygon(
    0% 0%,   100% 0%,   /* line 1: 0px wrapping width */
    100% 10%, 0% 10%,   /* line 2: full width */
    0% 10%,  100% 10%,  /* alternating... */
    100% 20%, 0% 20%
    /* etc. — creates a checkerboard-like wrapping pattern
       where every other line of consent text has 0px wrapping width */
  );
}

SA-CSS-SHP-001 (High). The attack is entirely layout-based — no property on the consent text element itself is manipulated. Every computed style of the consent text element (color, font-size, visibility, opacity, overflow) is correct. The consent text is present in the DOM with the correct content. Only the float sibling's shape-outside geometry collapses the wrapping area. Detection requires evaluating the rendered layout of consent text lines — specifically whether the available inline width per line is sufficient for the font size and whether the rendered line count and character density are consistent with the DOM text content.

/* Detection: shape-outside reducing consent wrapping area */
function detectShapeOutsideConsentDisplacement() {
  const CONSENT_KEYWORDS = ['authorize', 'grant', 'access', 'permission',
                            'agree', 'terms', 'third-party', 'transmit'];
  const findings = [];

  document.querySelectorAll('*').forEach(el => {
    const text = el.textContent.toLowerCase();
    if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
    if (el.textContent.trim().length < 30) return;

    // Check for floated siblings with shape-outside
    const parent = el.parentElement;
    if (!parent) return;

    Array.from(parent.children).forEach(sibling => {
      if (sibling === el) return;
      const sibCs = getComputedStyle(sibling);
      if (sibCs.float === 'none') return;

      const shapeOutside = sibCs.shapeOutside;
      if (!shapeOutside || shapeOutside === 'none') return;

      // Found a floated sibling with shape-outside adjacent to consent
      const sibRect = sibling.getBoundingClientRect();
      const parentRect = parent.getBoundingClientRect();

      // Check how much of the container width the float shape occupies
      // Approximation: if the float's rendered width is close to container width
      const floatWidthRatio = sibRect.width / Math.max(parentRect.width, 1);

      findings.push({
        vuln: 'SA-CSS-SHP-001',
        severity: 'HIGH',
        element: sibling,
        consentElement: el,
        detail: `float:${sibCs.float} sibling with shape-outside:${shapeOutside} adjacent to consent text; ` +
                `float is ${Math.round(sibRect.width)}px wide (${Math.round(floatWidthRatio * 100)}% of container ${Math.round(parentRect.width)}px); ` +
                `shape-outside geometry may compress consent wrapping area — requires layout analysis`
      });

      // Check rendered consent text for suspicious compression
      const consentRect = el.getBoundingClientRect();
      if (consentRect.width < 50 && el.textContent.trim().length > 30) {
        findings.push({
          vuln: 'SA-CSS-SHP-001-compressed',
          severity: 'CRITICAL',
          element: el,
          detail: `Consent text rendered width is ${Math.round(consentRect.width)}px for ${el.textContent.trim().length} chars — ` +
                  `text compressed to near-zero column by shape-outside wrapping; ` +
                  `sibling float has shape-outside:${shapeOutside}`
        });
      }
    });
  });

  return findings;
}

Attack 2: circle(0%) on full-width float displaces all consent text (SA-CSS-SHP-002)

A float with width: 100% of the container and shape-outside: circle(0%) defines a zero-radius circle exclusion area at the center of the float. The float's shape clips to its margin box (100% width, some height), but the wrapping boundary is the circle with 0% radius. Inline content cannot occupy the area inside the circle — but since the circle radius is 0%, the circle is a point, not an area. The real wrapping displacement comes from the float's full width: a float occupying the full container width means all inline content must wrap below it. Consent text that would normally flow beside the float is displaced to below the float's bottom edge. If the container has overflow: hidden and the float pushes the consent text below the container height, the text is clipped.

/* Attack: full-width float with circle(0%) shape displaces consent below container */
.consent-container {
  width: 300px;
  height: 60px;    /* container height — consent that flows below float is clipped */
  overflow: hidden;
}

.displacing-float {
  float: left;
  width: 100%;     /* occupies full container width */
  height: 30px;    /* occupies top 30px of container */
  background: transparent;
  shape-outside: circle(0%);
  /* circle(0%) = 0% of the float's reference box = 0px radius circle at center.
     The shape itself is a point.
     But the FLOAT's rectangular box still occupies 100% × 30px.
     The shape-outside value does not override the float's flow occupation —
     inline content must still wrap BESIDE the float's margin box.
     With a 100%-wide float: there is no "beside" — inline content is displaced BELOW.
     At height:30px into a height:60px container: 30px left for consent.
     If consent text requires more than 30px vertical space → overflow clipped. */
}

.consent-text {
  /* Normal styles — no suspicious properties */
  font-size: 14px;
  line-height: 1.6; /* each line = 22.4px */
  /* 3 lines of consent × 22.4px = 67.2px needed */
  /* Available: 30px (60px container - 30px float height) */
  /* Result: only 1-2 lines visible — consent truncated */
}

/* Variant: float with height:100% displaces all consent text below container */
.displacing-float-full {
  float: left;
  width: 100%;
  height: 100%; /* full container height */
  background: transparent;
  shape-outside: inset(0);
  /* All consent text displaced to below height:100% — outside overflow:hidden container */
}

Attack 3: inset(0px) on oversized float overflows wrapping area (SA-CSS-SHP-003)

shape-outside: inset(0px) defines the shape as the float's margin box itself. For a float wider than the container (e.g., width: 110% with negative margin), the margin box extends outside the container boundaries. Inline content wrapping around an oversized float with inset(0px) shape must flow around the full margin box — even the part that extends outside the container. The available wrapping area may be zero or negative. Consent text cannot flow to the right of the float (the float is wider than the container), so it displaces entirely below the float. If the float's height is set to match or exceed the container height, all consent text overflows below the container and is clipped by overflow: hidden.

/* Attack: oversized float with inset(0px) displaces consent to overflow */
.consent-container {
  width: 300px;
  height: 100px;
  overflow: hidden;
  position: relative;
}

.oversized-float {
  float: left;
  width: 110%;          /* wider than container */
  margin-left: -5%;     /* positioned to start slightly inside container */
  height: 80px;
  background: transparent;
  shape-outside: inset(0px);
  /* inset(0px) = shape is the float's own margin box
     float width: 110% of 300px = 330px
     Available wrapping width to the right of float: 300px - 330px = -30px (negative)
     Inline content cannot wrap to the right of this float at all.
     All consent text is displaced below the float's bottom edge.
     Float height: 80px. Container height: 100px. Remaining: 20px.
     Consent text requiring more than 20px vertical space → clipped. */
}

/* Detection note:
   Auditors checking float dimensions typically compare float to viewport width.
   A float with width:110% of its container, if the container is smaller than the viewport,
   may appear to be a reasonable proportional value — especially if the negative margin
   is set as a CSS custom property or computed value.
   shape-outside:inset(0px) on the float looks like a no-op (the shape is the margin box)
   and does not trigger alarm. The attack is the combination of oversized float + inset
   creating a larger-than-container exclusion zone. */

Attack 4: shape-margin extends exclusion zone beyond float dimensions (SA-CSS-SHP-004)

shape-margin adds an outward margin to the shape defined by shape-outside. The exclusion zone for inline wrapping is the union of the shape and its shape-margin. A float with visible dimensions of 20×100px but shape-margin: 200px creates a wrapping exclusion zone 220px wide (20px shape + 200px margin on each side). If the container is 300px wide, the exclusion zone is 220px — leaving only 80px for consent text to wrap. shape-margin values are not CSS margin values and do not appear in getComputedStyle(el).margin. Auditors checking float margins (to see if the float is pushed far from the container center) will not find the shape-margin value in standard margin properties.

/* Attack: shape-margin extends float exclusion zone silently */
.consent-container {
  width: 300px;
}

.shape-float {
  float: left;
  width: 20px;       /* visually small float — not alarming */
  height: 200px;
  background: transparent;
  shape-outside: inset(0px);  /* shape = float margin box = 20×200px */
  shape-margin: 250px;         /* extends exclusion zone by 250px on each side */
  /* Effective exclusion zone width: 20px + 2×250px = 520px
     Container width: 300px
     Available wrapping width: max(0, 300 - 520) = 0px
     Consent text: zero wrapping width → all lines have 0px available → clipped.

     CSS margin audit:
     getComputedStyle(float).margin     → '0px' (no CSS margin set)
     getComputedStyle(float).marginLeft → '0px'
     The shape-margin property IS readable via:
     getComputedStyle(float).shapeMargin → '250px'
     But auditors checking standard margin properties will not find this.
     shape-margin is NOT the same as CSS margin. */
}

/* Detection: shape-margin creates invisible exclusion zones */
function detectShapeMarginExclusionZone() {
  const findings = [];

  document.querySelectorAll('*').forEach(el => {
    const cs = getComputedStyle(el);
    if (cs.float === 'none') return;

    const shapeOutside = cs.shapeOutside;
    const shapeMargin = parseFloat(cs.shapeMargin) || 0;
    if (!shapeOutside || shapeOutside === 'none' || shapeMargin === 0) return;

    // Check if this float is adjacent to consent-bearing text
    const parent = el.parentElement;
    if (!parent) return;
    const consentText = parent.textContent.toLowerCase();
    const CONSENT_KEYWORDS = ['authorize', 'grant', 'access', 'permission', 'agree'];
    if (!CONSENT_KEYWORDS.some(k => consentText.includes(k))) return;

    const rect = el.getBoundingClientRect();
    const parentRect = parent.getBoundingClientRect();
    const exclusionZoneWidth = rect.width + shapeMargin * 2;
    const availableWidth = Math.max(0, parentRect.width - exclusionZoneWidth);

    findings.push({
      vuln: 'SA-CSS-SHP-004',
      severity: availableWidth < 50 ? 'HIGH' : 'MEDIUM',
      element: el,
      detail: `float:${cs.float} with shape-outside:${shapeOutside} and shape-margin:${shapeMargin}px; ` +
              `effective exclusion zone: ${Math.round(exclusionZoneWidth)}px wide; ` +
              `available wrapping width for consent: ${Math.round(availableWidth)}px; ` +
              `shape-margin does not appear in CSS margin properties — standard margin audits miss it`
    });
  });

  return findings;
}

SA-CSS-SHP combined detection: All four shape-outside consent bypass patterns share a key characteristic — they manipulate the wrapping geometry of a floated element rather than properties on the consent text itself. SkillAudit detects these by: (1) scanning for floated siblings of consent-bearing elements; (2) reading their shapeOutside, shapeMargin, and float dimensions; (3) computing the effective exclusion zone width relative to the container; and (4) comparing the available wrapping width against the minimum required for the consent font size. Any float-adjacent consent element with an available wrapping width below two characters' worth of space is flagged as a critical finding.

SkillAudit detection

HIGH SA-CSS-SHP-001: floated sibling with shape-outside: polygon(...) adjacent to consent text — polygon geometry compresses available wrapping area to near-zero. SkillAudit evaluates the polygon bounding box relative to the container width to compute available wrapping space per line.
HIGH SA-CSS-SHP-002: full-width float with shape-outside: circle(0%) displaces all consent text below the float and outside the overflow-clipped container. SkillAudit checks float width as a percentage of the container and evaluates consent text displacement relative to the container height.
HIGH SA-CSS-SHP-003: oversized float with shape-outside: inset(0px) creates an exclusion zone wider than the container — consent wrapping area is zero or negative. SkillAudit compares float rendered width against container width for all floats with shape-outside.
MEDIUM SA-CSS-SHP-004: shape-margin extends a float's exclusion zone far beyond its visible dimensions — standard CSS margin audits read zero margin but the effective wrapping exclusion zone may consume most of the container width. SkillAudit reads the non-standard shapeMargin computed property and adds it to the exclusion zone calculation.

Run SkillAudit to detect SA-CSS-SHP patterns in any MCP server before install. SkillAudit's layout-aware scanner evaluates shape-outside geometry relative to container dimensions and computes available wrapping width for consent text, catching float-based consent displacement attacks that pass all standard CSS property checks.