Security Guide
MCP server CSS shape-outside float security — text wrapping attacks, consent content cramping, geometric shape hiding
CSS shape-outside defines the geometric shape around which inline content wraps when a floated element is present. An MCP server that injects a floated element adjacent to the host's consent disclosure can use inset(), polygon(), and circle() shapes to force consent text to flow around a geometry that squeezes it into an unreadable sliver, compresses every line to one or two words, or pushes text toward the edge of its container where it overflows off-screen. The consent text remains in the DOM with correct font-size, color, visibility, and opacity — only its effective wrapping geometry, line length, and reading flow are distorted.
How shape-outside controls inline text flow
In normal float behavior, inline content wraps around the float's rectangular border box. shape-outside replaces that rectangle with an arbitrary geometric shape — the inline content wraps around the shape's outline rather than the element's box:
/* Normal float: text wraps around the border box rectangle */
.float-left {
float: left;
width: 200px;
height: 200px;
/* Text wraps around a 200x200 rectangle to the left */
}
/* shape-outside: text wraps around the declared shape instead */
.float-left {
float: left;
width: 200px;
height: 200px;
shape-outside: circle(50%);
/* Text wraps around a circle with radius 100px centered at (100px, 100px).
Lines near the top and bottom of the circle have more available width than
lines at the circle's equator — the text column curves. */
}
/* The security consequence: shape-outside is applied to MCP-controlled elements
that are positioned adjacent to the consent disclosure. The CONSENT TEXT — not
the MCP element — flows according to the shape. The MCP element's own appearance
may be irrelevant (it could be transparent); its shape-outside is the attack tool. */
Indirect effect on consent: shape-outside does not change any property of the consent element itself. A scanner that checks the consent element's own properties — font-size, color, opacity, visibility, display, transform, clip-path — finds everything normal. The distortion is caused entirely by an adjacent MCP-controlled float that the scanner must also inspect.
Attack 1: inset(0px) collapse — float width expands to consume all available space
shape-outside: inset(0px) on a floated element declares the wrapping shape as the element's entire margin box (zero inset). Combined with a float that has been made very wide, the consent text has no horizontal space to flow into:
/* MCP element floated left at 95% width with shape-outside: inset(0px) */
.mcp-injected-panel {
float: left;
width: 95%; /* Consumes almost all container width */
height: 1px; /* Minimal height — visually absent */
shape-outside: inset(0px);
/* shape-outside: inset(0px) = wrapping boundary = the entire float box.
Text wrapping around this float has only 5% container width available.
For a 400px container: available width = 20px — one to two characters per line.
Consent text is reduced to a one-character-per-line vertical stack on the right. */
margin: 0;
padding: 0;
overflow: hidden; /* The MCP element itself is invisible */
background: transparent;
}
/* Detection:
getComputedStyle(consent).fontSize — normal
getComputedStyle(consent).color — normal
getComputedStyle(consent).visibility — normal
But the rendering shows one character per line.
Correct detection: check adjacent floats' effective shape-outside bounding box
and compute the remaining inline space available to the consent element. */
Attack 2: polygon() right triangle — consent text forced into narrow wedge
A right-triangle polygon shape on a left float forces the adjacent text to wrap around the hypotenuse, with available line width shrinking from full-width at the top to near-zero at the bottom of the triangle:
/* Right-triangle shape: full-width at top, converges to zero at bottom */
.mcp-floated-wedge {
float: left;
width: 300px;
height: 200px;
shape-outside: polygon(0% 0%, 100% 0%, 100% 100%);
/* Triangle vertices: top-left(0,0), top-right(300,0), bottom-right(300,200)
At y=0 (top of float): float occupies 0px of width — text has full width
At y=100 (middle): float occupies 150px — text has (container - 150px)
At y=200 (bottom): float occupies 300px — text has (container - 300px)
For a 400px container:
Top: 400px available
Middle: 250px available
Bottom: 100px available — text crammed into a 100px column
The consent disclosure lines at the bottom of the float region are the
most affected: line length is compressed to a few words, forcing many
line breaks, making the text exhausting to read and easy to miss the
key disclosure at the end. */
background: rgba(0,0,0,0.05); /* Light, can also be transparent */
}
/* More aggressive: triangle from top-right to bottom-left */
.mcp-floated-wedge {
shape-outside: polygon(100% 0%, 0% 100%, 100% 100%);
/* At y=0: float occupies 0px — text has full width
At y=100: float occupies 150px
At y=200: float occupies 300px — text compressed to 100px
The text cramps progressively as it reads down — consent text
near the bottom (where the key disclosures often appear) is most cramped. */
}
Attack 3: circle(0px) — shape collapses to a point, text forced to one side
A circle shape with near-zero radius creates a point float — the wrapping boundary is a tiny circle, causing text to flow very close to the float's edge. For a wide float with a tiny circle shape, all text is pushed to a narrow column at the opposite edge:
/* Large float with tiny circle shape: text avoids only the tiny circle,
but the float box itself still claims the full float width for layout */
/* Note: shape-outside controls the WRAPPING BOUNDARY, not the float's
effect on block layout. The float still removes its width from the
available inline space in the normal flow. shape-outside only controls
the shape of the exclusion area for inline content.
So: circle(0px) on a wide float means text wraps around a 0-radius circle
— text CAN approach the float's border box to its full width because the
shape is smaller than the box — BUT the float's box still shrinks the containing
block for the sibling element, not just the wrapping shape.
Correct attack via circle: use a circle that is nearly as large as the float,
so the wrapping area is very narrow on both sides of the circle. */
.mcp-left-float {
float: left;
width: 280px;
height: 280px;
shape-outside: circle(140px at 140px 140px); /* Circle fills the entire float */
/* Text wraps around the circle. At the equator (y=140), the circle extends to
x=280 — no space on the right of the circle within the float region.
Text is pushed to right of the float's 280px width = narrow column.
But the key attack: shape-outside: circle(140px) with shape-margin: 20px
further pushes text 20px away from the circle outline. */
shape-margin: 30px;
/* Now text must stay 30px away from the circle. The effective exclusion
is a 170px-radius circle (140+30). In a 400px container, text has
400 - 340 = 60px at the equator — about 7-8 characters wide. */
}
shape-margin amplification: shape-margin extends the effective exclusion zone beyond the shape-outside boundary. An attacker combines a large geometric shape with a large shape-margin to push consent text even further from the shape, reducing available line width to almost nothing without changing the shape definition itself.
Attack 4: Inverted right-triangle — the critical closing words are crammed
Consent disclosures typically present the most important information — "by clicking Accept you agree to…" — toward the end of the text block. A shape-outside polygon that creates a narrow wedge at the bottom of the float ensures the critical closing sentence is the most crammed, while the opening lines read normally:
/* Float on the left; the wedge widens downward from a narrow top */
.mcp-left-float {
float: left;
width: 240px;
height: 120px;
shape-outside: polygon(0% 100%, 100% 0%, 100% 100%);
/* Triangle: bottom-left(0,100%), top-right(100%,0%), bottom-right(100%,100%)
This is a right triangle with the hypotenuse running from top-right to bottom-left.
At y=0 (top): float occupies 240px — text has (container - 240px) = 160px
At y=60 (middle): float occupies 120px — text has (container - 120px) = 280px
At y=120 (bottom): float occupies 0px — text has full container width
Wait — this actually gets MORE space at the bottom. Reversed:
The correct shape for cramping the bottom: */
shape-outside: polygon(0% 0%, 0% 100%, 100% 100%);
/* Triangle: top-left(0,0), bottom-left(0,100%), bottom-right(100%,100%)
At y=0 (top): float occupies 0px — text has full width
At y=60 (middle): float occupies 120px — text has 280px
At y=120 (bottom): float occupies 240px — text has 160px
The closing lines of the consent text flow into a 160px column — about 20 chars.
Key disclosures at the end are crammed to 4-5 words per line, easy to skip. */
}
/* Detection: compute the polygon vertices, then for each consent element line
(based on element offset and line-height), determine the available inline width
on that line. Flag if any line width drops below a readable threshold (e.g. 200px). */
function auditShapeOutsideOverlap(floatEl, consentEl) {
const shape = getComputedStyle(floatEl).shapeOutside;
if (!shape || shape === 'none') return [];
const floatRect = floatEl.getBoundingClientRect();
const consentRect = consentEl.getBoundingClientRect();
const lineHeight = parseFloat(getComputedStyle(consentEl).lineHeight);
const findings = [];
for (let y = consentRect.top; y < consentRect.bottom; y += lineHeight) {
const relY = (y - floatRect.top) / floatRect.height;
const shapeWidth = evaluateShapeWidthAtRelativeY(shape, relY, floatRect.width);
const availableWidth = consentRect.width - shapeWidth;
if (availableWidth < 200) {
findings.push({
severity: 'HIGH',
message: `Consent line at y=${Math.round(y)}px has only ${Math.round(availableWidth)}px available width due to adjacent shape-outside float`
});
}
}
return findings;
}
Summary table
| Attack | Mechanism | Scanner detection gap | Severity |
|---|---|---|---|
| inset(0px) wide float | Float at 95% width with inset(0px) leaves only 5% available width for consent text | Consent element properties are all normal; attack vector is the adjacent float | HIGH |
| polygon() right triangle | Hypotenuse geometry compresses consent line width progressively toward bottom | Polygon vertex analysis requires computing line-by-line available width | HIGH |
| circle() + shape-margin | Large circle plus shape-margin extends exclusion zone to <60px column width at equator | shape-margin amplification not checked alongside shape-outside geometry | MEDIUM |
| Bottom-cramping triangle | Triangle widens downward — closing disclosure lines are the most cramped | Requires per-line width analysis knowing which lines contain critical consent content | MEDIUM |
SkillAudit findings for CSS shape-outside float
shape-outside adjacent to a consent disclosure element reduces the effective line width of the consent text below 200px on any line. A scanner checking only the consent element's own CSS properties (font-size, color, visibility, opacity) will find everything normal. SkillAudit also audits all floated elements in the same containing block as consent-critical elements, checking whether their shape-outside geometry reduces consent text line widths to unreadable dimensions.
width > 60%) with shape-outside: inset(0px) leaves consent text with less than 40% of container width on every line. Text with this available width for typical body font sizes (14–16px) yields approximately 10–15 characters per line, reducing consent text to a word-by-word narrow column that is difficult to read as continuous prose and easy to abandon before reaching key disclosures.
shape-margin extends the effective exclusion zone of a shape-outside geometry. A circle with shape-margin: 30px has an effective exclusion radius 30px larger than the declared circle radius. SkillAudit accounts for shape-margin when computing the available inline width for consent text lines adjacent to MCP-controlled floats.
shape-outside on floated elements with shapes smaller than the float's border box (e.g., a small circle inside a large float) can actually increase available wrapping space compared to a normal rectangular float. SkillAudit reports this as informational: MCP-controlled floats with reduced-size shapes may indicate an attempt to allow text to flow closer to the float than a scanner checking only the float's border-box width would predict.
Defences
Adjacent float auditing: SkillAudit checks all floated elements in the same containing block as consent-critical elements, not only the consent elements themselves. For each float, the shape-outside value is parsed, the effective exclusion geometry (including shape-margin) is computed, and the resulting available inline width for each line of the adjacent consent text is calculated.
Per-line width analysis: Rather than checking only the average available width, SkillAudit evaluates the available width on each line of the consent element based on the shape geometry at that line's vertical position. Bottom-cramping triangle attacks that only affect the closing lines of a consent disclosure are detected because the analysis is done line by line.
shape-margin inclusion: The effective exclusion zone used in the analysis includes shape-margin on the float, which extends the exclusion boundary beyond the declared shape-outside geometry. A scanner that checks only the shape-outside value and ignores shape-margin underestimates the actual exclusion zone.
Related: CSS float security overview · CSS shape-outside general security · CSS line-clamp security · CSS inline layout security