← Blog

MCP server CSS clip-path function consent security

CSS clip-path defines the visible rendering region of an element using shape functions. When a degenerate shape is supplied — one that collapses to zero area — the element becomes completely invisible while retaining its layout footprint. getBoundingClientRect() still returns full width and height. getComputedStyle returns an opaque string value for clip-path that must be parsed to evaluate the shape geometry. Dimension-checking auditors that confirm element presence via bounding-box size are systematically bypassed. Four SA-CSS-CPF patterns document the primary degenerate-shape attack vectors.

By SkillAudit  |  30 Oct 2026

The CSS clip-path property clips an element's rendered output to a specified shape. Pixels outside the shape are not painted; pixels inside the shape are painted normally. Critically, clipping is a rendering operation, not a layout operation: the element still occupies its full layout area. This means all dimension-based presence checks — offsetWidth, offsetHeight, getBoundingClientRect() — return the unclipped dimensions. Only pixels-on-screen and visibility checks that account for clip geometry catch degenerate clip-path attacks.

The four functions used in clip-path attacks — polygon(), inset(), circle(), and ellipse() — each have degenerate parameter combinations that produce zero visible area. Because the function argument is a coordinate string, not a numeric property, programmatic detection requires parsing the getComputedStyle(el).clipPath string value to evaluate the shape geometry — or using document.elementFromPoint() to probe for pixel-level visibility.

SA-CSS-CPF-001: clip-path: polygon(0 0, 0 0, 0 0) — degenerate triangle

A CSS polygon() shape with all vertices at the same point defines a degenerate triangle with zero area. All three vertices coinciding at the origin (0 0, 0 0, 0 0) reduces the clipping polygon to a point. No pixels fall within a zero-area polygon, so the element renders zero visible pixels. The element layout is unaffected: it still occupies its declared width and height in the document flow.

/* Attack: degenerate polygon — all vertices at origin */
.consent-text {
  clip-path: polygon(0 0, 0 0, 0 0);
  /* All three vertices coincide at top-left corner.
     The polygon has zero area: no pixels are visible.
     The element retains full layout dimensions. */
}

/* Variations of degenerate polygon attacks: */
.consent-text { clip-path: polygon(50% 50%, 50% 50%, 50% 50%); }
/* All vertices at center — zero area */

.consent-text { clip-path: polygon(0 50%, 100% 50%, 100% 50%); }
/* Degenerate: third vertex collapses edge to a line (zero height) */

/* Why auditors miss this:
   1. getBoundingClientRect(consentTextEl).width  → e.g. 380 (normal)
   2. getBoundingClientRect(consentTextEl).height → e.g. 24 (normal)
   3. getComputedStyle(consentTextEl).display → 'block' (not hidden)
   4. getComputedStyle(consentTextEl).visibility → 'visible' (not hidden)
   5. getComputedStyle(consentTextEl).opacity → '1' (not transparent)
   6. getComputedStyle(consentTextEl).color → 'rgb(0,0,0)' (normal)
   → All standard checks pass! Element appears present.

   Correct detection:
   getComputedStyle(consentTextEl).clipPath
   → "polygon(0px 0px, 0px 0px, 0px 0px)" (must parse this string)

   Parse polygon vertices — if all vertices are equal → CRITICAL
   Or: document.elementFromPoint(x, y) where (x,y) is center of bounding box
   → returns a different element (clip-path has removed the element from the hit area) */

Bounding-box checks are bypassed: getBoundingClientRect() is the most common programmatic check for element visibility. clip-path attacks exploit the layout-vs-rendering split: the element occupies real layout space (bounding box non-zero) but renders zero visible pixels. The only reliable checks are parsed clipPath string evaluation or pixel-level hit-testing via document.elementFromPoint().

CRITICAL
SA-CSS-CPF-001 — clip-path: polygon(0 0, 0 0, 0 0): degenerate triangle at origin; zero visible area; bounding box preserved. Detection: parse getComputedStyle.clipPath polygon vertices for coincident points, or elementFromPoint() hit test at element center → CRITICAL.

SA-CSS-CPF-002: clip-path: inset(50% 50%) — centered zero-area inset

clip-path: inset() defines a rectangular clipping region by specifying insets from each edge. inset(50% 50%) means: inset 50% from top, 50% from bottom (first value applies to top and bottom), and 50% from left, 50% from right (second value applies to left and right). The remaining visible area is 0% of the element width and 0% of the element height. The inset rectangle collapses to a point at the element center. The property value inset(50% 50%) is plausible-looking: it resembles common CSS patterns like inset(0) (fill container) or inset(10px) (slight crop), and is easily confused with padding or margin shorthand.

/* Attack: inset collapse to zero at center */
.consent-text {
  clip-path: inset(50% 50%);
  /* inset(top/bottom left/right):
     top + bottom = 50% + 50% = 100% of height → visible height = 0
     left + right = 50% + 50% = 100% of width  → visible width  = 0
     Remaining clip region: a point at element center. */
}

/* Other zero-area inset patterns: */
.consent-text { clip-path: inset(100%); }
/* All four sides inset 100% → zero area */

.consent-text { clip-path: inset(0 100%); }
/* Left+right: 100% each → zero width, full height → invisible line */

.consent-text { clip-path: inset(51% 0 51% 0); }
/* Top 51% + bottom 51% = 102% → overflow; browser clamps to 100% → zero visible height */

/* Parsing inset() to detect zero-area:
   getComputedStyle(consentTextEl).clipPath
   → "inset(50% 50%)" or computed pixel equivalents

   Parse the inset arguments:
   - 1 value:  top=bottom=left=right = value
   - 2 values: top=bottom=arg1, left=right=arg2
   - 4 values: top=arg1, right=arg2, bottom=arg3, left=arg4

   visible_width  = element.offsetWidth  * (1 - leftInsetFraction - rightInsetFraction)
   visible_height = element.offsetHeight * (1 - topInsetFraction  - bottomInsetFraction)
   if visible_width ≤ 0 OR visible_height ≤ 0 → CRITICAL */

Inset values look like layout code: clip-path: inset(50% 50%) resembles a responsive centering pattern to a casual reviewer. Automated detection must evaluate the arithmetic: sum the opposing inset fractions and flag any case where the sum reaches or exceeds 100% on either axis.

CRITICAL
SA-CSS-CPF-002 — clip-path: inset(50% 50%): opposing insets sum to 100%; visible area = 0; element retains layout dimensions. Detection: parse inset arguments, sum opposing pairs; flag when sum ≥ 100% on either axis → CRITICAL.

SA-CSS-CPF-003: clip-path: circle(0) — zero-radius circle

clip-path: circle() clips the element to a circle defined by a radius and optional center point. circle(0) specifies a radius of zero at the default center (50% 50%). A zero-radius circle has zero area: no pixels fall within it. The element is invisible. The center position can be specified explicitly — circle(0 at 50% 50%), circle(0 at center), circle(0px at 0% 0%) — with identical result: zero visible area regardless of center position when radius is zero.

/* Attack: zero-radius circle */
.consent-text {
  clip-path: circle(0);
  /* circle(radius): radius = 0 → no pixels visible */
}

/* Explicit center variations: */
.consent-text { clip-path: circle(0 at center); }
.consent-text { clip-path: circle(0px at 50% 50%); }
.consent-text { clip-path: circle(0 at 0% 0%); }
/* All equivalent: zero-radius circle → zero visible area */

/* Near-zero radius (sub-pixel attack): */
.consent-text { clip-path: circle(0.1px at center); }
/* 0.1px radius → effectively invisible at any normal rendering resolution */
/* Must threshold: radius < 2px → treat as degenerate → CRITICAL */

/* Detection approach for circle():
   getComputedStyle(consentTextEl).clipPath
   → "circle(0px at 50% 50%)" (browser normalizes to px values)

   Parse radius from circle() string:
   const match = clipPath.match(/^circle\(([^)]+)\)/);
   const radiusStr = match[1].split(' at ')[0].trim();
   const radiusPx = parseFloat(radiusStr); // after 'px' unit
   if (radiusPx <= 1) → CRITICAL (zero or sub-pixel radius)

   Note: browser may resolve percentage radii to px —
   'circle(0%)' → 'circle(0px at 50% 50%)' in getComputedStyle
   check for 0px directly */

Sub-pixel radii are effectively zero: A 0.1px radius circle is mathematically non-zero but renders no visible pixels at any screen resolution. Detection thresholds should treat radii below 2px as degenerate. The browser may normalize the radius to pixels in getComputedStyle output, making it easier to evaluate numerically.

CRITICAL
SA-CSS-CPF-003 — clip-path: circle(0): zero-radius circle; no visible pixels at any center position; bounding box unchanged. Detection: parse radius from getComputedStyle.clipPath circle() value; flag radius ≤ 1px → CRITICAL.

SA-CSS-CPF-004: clip-path: ellipse(0 0 at 50% 50%) — zero-radii ellipse

clip-path: ellipse() clips the element to an ellipse defined by horizontal radius, vertical radius, and center position. ellipse(0 0 at 50% 50%) specifies both radii as zero, producing a zero-area ellipse at element center. As with circle(0), the element is completely invisible. ellipse() offers an additional attack surface compared to circle(): the two radii can be set independently, allowing one axis to be zero while the other is non-zero — producing an invisible line rather than a point.

/* Attack: zero-radii ellipse */
.consent-text {
  clip-path: ellipse(0 0 at 50% 50%);
  /* ellipse(rx ry at cx cy): rx=0, ry=0 → zero area */
}

/* Partial degeneracy — one radius zero: */
.consent-text { clip-path: ellipse(100% 0 at 50% 50%); }
/* ry = 0: horizontal line (height=0) → invisible despite non-zero width */

.consent-text { clip-path: ellipse(0 100% at 50% 50%); }
/* rx = 0: vertical line (width=0) → invisible despite non-zero height */

/* Detection for ellipse():
   getComputedStyle(consentTextEl).clipPath
   → "ellipse(0px 0px at 50% 50%)" or with resolved values

   Parse rx and ry:
   const match = clipPath.match(/^ellipse\(([^)]+)\)/);
   const parts = match[1].split(' at ')[0].trim().split(/\s+/);
   const rx = parseFloat(parts[0]); // horizontal radius
   const ry = parseFloat(parts[1]); // vertical radius
   if (rx <= 1 || ry <= 1) → CRITICAL (degenerate ellipse)

   Complete clip-path degenerate shape detection checklist:
   - polygon(): check if all vertices are equal OR if shape has zero area (shoelace formula)
   - inset(): check if opposing inset pairs sum to ≥ 100%
   - circle(): check if radius ≤ 1px
   - ellipse(): check if rx ≤ 1px OR ry ≤ 1px */

Cross-function detection strategy: Degenerate clip-path detection requires parsing the shape function type and evaluating its parameters arithmetically. The reliable cross-function approach is document.elementFromPoint(cx, cy) where cx, cy is the center of the element's bounding box: if the returned element is not the consent text element (or a child), the element has been clipped away from that coordinate — regardless of which shape function was used.

CRITICAL
SA-CSS-CPF-004 — clip-path: ellipse(0 0 at 50% 50%): zero both radii; zero area; also partial degeneracy (one radius zero = invisible line). Detection: parse rx and ry from ellipse() value; flag either ≤ 1px → CRITICAL.

Summary of SA-CSS-CPF findings

IDSeverityclip-path valueDegenerate conditionDetection
SA-CSS-CPF-001 CRITICAL polygon(0 0, 0 0, 0 0) All vertices coincide — zero-area polygon Parse polygon vertices; check for coincident points or zero-area via shoelace formula
SA-CSS-CPF-002 CRITICAL inset(50% 50%) Opposing insets sum to 100% on both axes Parse inset args; sum opposing pairs; flag when sum ≥ 100% on either axis
SA-CSS-CPF-003 CRITICAL circle(0) Radius = 0 (or ≤ 1px) Parse radius from circle() value; flag radius ≤ 1px
SA-CSS-CPF-004 CRITICAL ellipse(0 0 at 50% 50%) Both radii zero; or either radius zero (invisible line) Parse rx, ry; flag either ≤ 1px; cross-function: elementFromPoint() hit test

Related pages