Security Guide

MCP server CSS clip-path consent security — inset(0 0 100% 0) zero-area clip, degenerate polygon, circle(0px) zero-radius, ancestor clip-path bypass

CSS clip-path hides consent elements without changing any of the properties standard audits check. The element remains in the DOM, retains its layout dimensions (getBoundingClientRect() reports the original width and height), and has fully legible color, font-size, and opacity. Only the painted area is zero. Four clip-path patterns create this invisibility for consent text: a full-height inset() cutoff, a degenerate zero-area polygon, a zero-radius circle, and ancestor-level clip-path that element-level audits miss entirely.

How clip-path differs from other hiding techniques

clip-path is a paint-only operation: it defines which pixels of an element are rendered, without affecting the element's layout box, stacking order, or event delivery (see clip-path pointer event security for that attack surface). The element's offsetWidth, offsetHeight, getBoundingClientRect(), and scrollHeight all return the full unclipped dimensions. This separates clip-path consent attacks from height:0 or overflow:hidden attacks, which change layout dimensions and are more easily detected by dimension checks.

Attack 1: clip-path: inset(0 0 100% 0) clips away the full bottom area (SA-CSS-CPC-001)

inset() defines a rectangular clip by specifying offsets from each edge (top, right, bottom, left). inset(0 0 100% 0) sets the bottom inset to 100% of the element height — the clip region has top edge at 0, bottom edge at 0 (100% up from the bottom), right and left at 0. This collapses the visible area to a zero-height slice at the very top of the element. Equivalently, inset(100% 0 0 0) clips from the top, and inset(0 100% 0 0) clips from the right. The element's full getBoundingClientRect() dimensions are unchanged — only the paint region is zero.

/* Full-height inset clip — zero visible area, original layout dimensions */
.consent-text {
  /* No suspicious color, opacity, visibility, or display properties */
  color: #1a1a1a;
  font-size: 14px;
  line-height: 1.6;

  /* The attack: */
  clip-path: inset(0 0 100% 0);
  /* Insets: top=0, right=0, bottom=100%, left=0
     Visible area: from top-edge=0 to bottom-edge=(height - 100% of height) = 0px
     Result: zero-height clip region → nothing painted */
}

/* Detection comparison:
   consent.getBoundingClientRect()     → { width: 400, height: 72 }  ← looks fine
   consent.offsetWidth                 → 400                          ← looks fine
   getComputedStyle(consent).clipPath  → "inset(0px 0px 100% 0px)"   ← catches it IF checked
   getComputedStyle(consent).color     → "rgb(26, 26, 26)"            ← looks fine
   getComputedStyle(consent).opacity   → "1"                          ← looks fine
   getComputedStyle(consent).visibility → "visible"                   ← looks fine

   Standard dimension and color checks all pass.
   Only computed clip-path check reveals the attack. */

/* Variants: */
/* clip-path: inset(100% 0 0 0)  — clips from top */
/* clip-path: inset(50% 0 50% 0) — collapses to zero between top+bottom insets */
/* clip-path: inset(0 100% 0 0)  — clips from right (zero-width column) */

Zero visible area, unchanged layout box: Unlike height:0 or display:none, the inset(0 0 100% 0) clip does not change the element's layout footprint. It occupies the same space in the document flow and passes every dimension check. SkillAudit evaluates the resolved clip-path shape on consent elements and flags clip shapes that produce zero or near-zero visible area.

Attack 2: Degenerate polygon(0 0, 0 0, 0 0) defines zero-area clip (SA-CSS-CPC-002)

A valid CSS polygon requires at least three vertices. polygon(0 0, 0 0, 0 0) specifies three vertices, all at the same point (0,0 of the element's border box). The resulting polygon has zero area — all three vertices collapse to a single point. The browser accepts this as a valid clip-path value (no parse error), renders zero pixels of the element, and preserves all layout properties. More obfuscated variants place vertices at the same coordinates using different units: polygon(0% 0%, 0px 0px, 0em 0em) — these all resolve to the same zero-area point but are less immediately obvious in a computed style check.

/* Degenerate polygon — zero-area, all vertices at same point */
.consent-text {
  clip-path: polygon(0 0, 0 0, 0 0);
  /* Three valid vertices, all at (0,0) → zero-area polygon → nothing painted */
}

/* More obfuscated variants: */
.consent-text {
  /* Same point in different units: */
  clip-path: polygon(0% 0%, 0px 0px, 0em 0em);
}

.consent-text {
  /* Near-zero area instead of exactly zero — harder to detect with threshold check */
  clip-path: polygon(0 0, 0.1px 0, 0 0.1px);
  /* Triangle with 0.1px sides → ~0.005 px² area — imperceptible to human */
}

.consent-text {
  /* Single-point collapse using calc: */
  clip-path: polygon(
    calc(50% - 0px) calc(50% - 0px),
    calc(50% + 0px) calc(50% - 0px),
    calc(50% + 0px) calc(50% + 0px)
  );
  /* All three vertices resolve to (50%, 50%) — zero-area triangle at center */
}

/* getComputedStyle check: "polygon(0px 0px, 0px 0px, 0px 0px)" → should be flagged */

Attack 3: circle(0px at 50% 50%) zero-radius clip hides element (SA-CSS-CPC-003)

circle(r at cx cy) defines a circular clip region with radius r at center (cx, cy). Setting r = 0px creates a zero-radius circle — mathematically a point — at the element's center. Zero pixels are inside a circle of radius zero, so nothing is painted. The computed style returns "circle(0px at 50% 50%)" verbatim. Similar to the degenerate polygon attack, the element retains full layout dimensions. The 0px radius can be specified via a CSS custom property (circle(var(--consent-radius) at 50% 50%)) to obscure the zero value in static analysis.

/* Zero-radius circle clip */
.consent-text {
  clip-path: circle(0px at 50% 50%);
  /* Radius = 0px → zero-area circle → nothing painted */
}

/* Variant using custom property to hide the zero value: */
:root {
  --consent-clip-radius: 0px; /* set once at load, never changes */
}
.consent-text {
  clip-path: circle(var(--consent-clip-radius) at 50% 50%);
  /* getComputedStyle shows: "circle(var(--consent-clip-radius) at 50% 50%)"
     Static audit must resolve --consent-clip-radius to detect the zero value */
}

/* JS variant: radius set to 0 at mousedown */
installBtn.addEventListener('mousedown', () => {
  consentEl.style.clipPath = 'circle(0px at 50% 50%)';
});
installBtn.addEventListener('click', () => {
  setTimeout(() => { consentEl.style.clipPath = ''; }, 300);
});

/* Ellipse variant: */
.consent-text {
  clip-path: ellipse(0px 0px at 50% 50%);
  /* Both radii = 0 → zero-area ellipse → nothing painted */
}

Custom property chaining: When the zero value is set via var(--consent-clip-radius), a naive computed style check sees the variable reference, not the resolved value. SkillAudit resolves custom property chains before evaluating clip-path shape geometry, flagging effective radius values below a minimum readable threshold on consent elements.

Attack 4: clip-path on consent ancestor bypasses element-level audit (SA-CSS-CPC-004)

An audit that checks only the consent element's own clip-path misses the case where clip-path is applied to an ancestor. When an ancestor has clip-path: inset(0 0 100% 0) or any zero-area clip, all of its descendants — including the consent element — are clipped to zero visible area. The consent element itself has clip-path: none (default) and passes an element-level clip check. The attack therefore bypasses audits that only inspect the direct computed properties of the consent element without traversing its ancestors.

/* Ancestor clip-path hides all descendants including consent */

/* HTML structure: */
/* 
← clip-path applied here */ /* */ /* */ /*
*/ /* CSS: */ .install-section { clip-path: polygon(0 0, 0 0, 0 0); /* zero-area clip on ancestor */ /* .consent-text has clip-path: none — element audit passes */ /* But the ancestor's clip clips everything inside to zero visible area */ } .consent-text { /* No clip-path — element-level audit finds nothing suspicious */ color: #1a1a1a; /* dark → ✓ */ font-size: 14px; /* legible → ✓ */ clip-path: none; /* no clip → ✓ */ /* Reality: completely invisible due to ancestor clip */ } /* More subtle: clip on a mid-level ancestor between consent and install button */ .consent-container { /* Two levels above consent text, one below install-section */ clip-path: circle(0px at 50% 50%); } /* Detection: requires walking the full ancestor chain checking clip-path values on any element that contains the consent node */ // JavaScript audit must traverse parentElement chain: function hasAncestorClipPath(el) { let node = el.parentElement; while (node && node !== document.body) { const cp = getComputedStyle(node).clipPath; if (cp && cp !== 'none') { // evaluate whether cp produces zero area... return true; } node = node.parentElement; } return false; }
Attack IDMechanismElement-level audit resultRequired detection method
SA-CSS-CPC-001inset(0 0 100% 0) — zero-height clip regionDimensions normal, color normal, clip-path property exists → detectable if checkedEvaluate resolved inset clip area on consent element
SA-CSS-CPC-002polygon(0 0, 0 0, 0 0) — zero-area degenerate polygonDimensions normal, clip-path exists → detectable if geometry evaluatedCompute polygon area; flag when area < minimum threshold
SA-CSS-CPC-003circle(0px at 50% 50%) — zero-radius circleDimensions normal, radius = 0px in computed style → detectable if radius checkedResolve custom-property-chained radius values; flag radius = 0
SA-CSS-CPC-004Zero-area clip on consent ancestorConsent element has clip-path: none → passes element-level auditTraverse ancestor chain; evaluate all ancestor clip-path values

SkillAudit findings for CSS clip-path consent attacks

HIGHSA-CSS-CPC-001: inset() full-dimension clip: clip-path: inset(0 0 100% 0) and variants produce zero visible area while preserving layout dimensions — all standard dimension and color checks pass. SkillAudit evaluates the resolved inset clip shape and flags any inset() value where top + bottom offsets ≥ 100% or left + right offsets ≥ 100% on consent-containing elements.
HIGHSA-CSS-CPC-002: Degenerate polygon zero-area clip: clip-path: polygon(0 0, 0 0, 0 0) and near-zero area triangles produce imperceptibly small painted areas. SkillAudit computes the polygon area from resolved vertex coordinates and flags polygons with less than a minimum visible threshold on consent elements.
HIGHSA-CSS-CPC-003: Zero-radius circle/ellipse clip: circle(0px) and ellipse(0px 0px) produce zero visible area. When the radius is set via CSS custom property, SkillAudit resolves the variable chain to the effective numeric value before checking the radius magnitude.
MEDIUMSA-CSS-CPC-004: Ancestor clip-path bypassing element-level detection: clip-path on any ancestor element clips all descendants including consent text. Audits that check only the consent element's direct computed clip-path miss this pattern. SkillAudit traverses the full ancestor chain for all consent-containing elements and evaluates each ancestor's clip-path geometry.

Defences

Evaluate resolved clip shape geometry, not just presence: Checking clip-path !== 'none' is insufficient — the key test is whether the resolved shape has non-trivial visible area. Parse and evaluate the geometry of inset(), polygon(), circle(), and ellipse() values to confirm the visible area meets a minimum percentage of the element's layout dimensions.

Traverse the full ancestor chain: Consent visibility audits must walk the ancestor chain from the consent element to the document body, evaluating each ancestor's clip-path value. An ancestor clip-path of zero area is equivalent to hiding the consent element regardless of the element's own properties.

Resolve CSS custom property chains before evaluating: var(--consent-clip-radius) as a clip-path argument requires resolving the variable to its effective value. SkillAudit resolves custom property chains using getComputedStyle's property value resolution before geometric evaluation.

Use IntersectionObserver as a complement: Geometric analysis catches known clip patterns; IntersectionObserver with a threshold of 0.1 provides a render-based confirmation that the consent element is actually visible in the viewport, catching novel clip shapes that geometric parsing might miss.

Related: CSS clip-path pointer event security · CSS mask consent security · CSS display:contents consent security