MCP server SVG clipPath security
SVG clip-path applied to a consent text element defines a clipping region: only pixels within the path are rendered. A zero-area clip path clips every pixel, producing a fully transparent element. Unlike CSS visibility: hidden or opacity: 0, a clip-path attack is invisible to the most common consent auditing checks — getBoundingClientRect() returns the layout dimensions, not the clip region, so the element appears to have positive size.
Finding SA-CLIP-001: zero-area clip path eliminates all consent pixels
<clipPath> element containing a degenerate path (M 0,0 Z — a path that goes to 0,0 and immediately closes with no area) or a collapsed rectangle (<rect width="0" height="0"/>) defines a clip region with zero area. Applied to a consent text element, every pixel falls outside the clip region and is rendered as transparent. The element still exists in the DOM, has non-empty textContent, and returns positive dimensions from getBoundingClientRect(). The clip only affects which pixels are painted.<defs>
<clipPath id="zero-clip">
<path d="M 0,0 Z"/> <!-- zero-area path -->
</clipPath>
</defs>
<text clip-path="url(#zero-clip)">
I authorize this MCP server to access my email and calendar.
</text>
DOM checks that pass despite zero rendering: getBoundingClientRect() — positive dimensions (layout not affected). getComputedStyle(el).display — not none. getComputedStyle(el).visibility — visible. getComputedStyle(el).opacity — 1. el.textContent — non-empty. el.offsetWidth/offsetHeight — positive. Only getComputedStyle(el).clipPath returns a URL reference, and only following that URL and computing the path's bounding box reveals the zero-area attack.
Finding SA-CLIP-002: CSS d property morphs clip path to zero at interaction
d property controls the d attribute of SVG <path> elements via CSS and is animatable. A clip path that starts with a safe full-coverage rectangle transitions to a zero-area path via a CSS keyframe animation triggered at button interaction. At parse time, the clip path covers the full consent element. At the moment the user interacts with the submit button, a CSS animation morphs the clip path to zero-area, erasing the consent text for the duration of the animation (or permanently if animation-fill-mode: forwards).<defs>
<clipPath id="morph-clip">
<path id="clip-path-el" d="M 0,0 L 300,0 L 300,100 L 0,100 Z"/>
</clipPath>
</defs>
<style>
#submitBtn:active ~ #clip-path-el,
#submitBtn:focus ~ #clip-path-el {
d: path("M 0,0 Z"); /* CSS d property animation */
animation: collapse 0.05s forwards;
}
@keyframes collapse {
to { d: path("M 0,0 Z"); }
}
</style>
Finding SA-CLIP-003: clipPathUnits=userSpaceOnUse coordinate mismatch
clipPathUnits="userSpaceOnUse", the clip path coordinates are in the coordinate system of the element referencing the clip path — not the clip path's own local coordinate system. If the consent element is inside a transformed ancestor (transform="scale(0.01)"), the user-space coordinates are scaled: a clip path with coordinates M 0,0 L 300,0 L 300,100 in user space refers to a region that is 300 × 100 in the transformed system but only 3 × 1 pixels in screen space — entirely too small to cover the consent text. The element appears clipped to near-zero size while the clip path's written coordinates look reasonable.<defs>
<clipPath id="coord-attack" clipPathUnits="userSpaceOnUse">
<!-- 300×100 in user space, but ancestor is scaled ×0.01 -->
<rect x="0" y="0" width="300" height="100"/>
</clipPath>
</defs>
<g transform="scale(0.01)"> <!-- parent scales consent space down -->
<text clip-path="url(#coord-attack)">Consent text...</text>
</g>
Finding SA-CLIP-004: nested clipPath double-clipping produces zero intersection
<clipPath> element can itself reference another <clipPath> via clip-path on its children. Two clip paths that define non-overlapping regions produce a zero-area intersection when applied in sequence. For example, clip path A clips to the left half of the element; clip path B clips to the right half. An element that references A, where A's children reference B, is clipped to the intersection of left-half and right-half — an empty set. Each individual clip path appears legitimate; the attack lies in the non-overlapping combination.<defs>
<!-- Clip A: left half -->
<clipPath id="clip-left">
<rect x="0" y="0" width="50%" height="100%"/>
</clipPath>
<!-- Clip B: right half, applied to clip-left's children -->
<clipPath id="clip-right">
<rect clip-path="url(#clip-left)" x="50%" y="0" width="50%" height="100%"/>
</clipPath>
</defs>
<!-- Intersection of left-half ∩ right-half = empty set -->
<text clip-path="url(#clip-right)">Consent disclosure...</text>
Detection: computing clip path bounding area
function checkSVGClipPaths(consentEl) {
const risks = [];
const clipPathRef = consentEl.getAttribute('clip-path')
|| getComputedStyle(consentEl).clipPath;
if (!clipPathRef || clipPathRef === 'none') return risks;
const idMatch = clipPathRef.match(/url\(#([^)]+)\)/);
if (!idMatch) return risks;
const clipPathEl = document.getElementById(idMatch[1]);
if (!clipPathEl) return risks;
// Check clipPathUnits for coordinate mismatch
const units = clipPathEl.getAttribute('clipPathUnits') || 'userSpaceOnUse';
if (units === 'userSpaceOnUse') {
// Check for scaled ancestor transform
let ancestor = consentEl.parentElement;
while (ancestor) {
const t = ancestor.getAttribute('transform') || '';
if (/scale\s*\(\s*0\.0[0-9]/.test(t)) {
risks.push({ finding: 'SA-CLIP-003', severity: 'high',
note: 'clipPathUnits=userSpaceOnUse with scaled ancestor' });
}
ancestor = ancestor.parentElement;
}
}
// Compute bounding box of clip path shapes
const shapes = clipPathEl.querySelectorAll('rect, circle, ellipse, path, polygon');
let totalArea = 0;
for (const shape of shapes) {
try {
const bbox = shape.getBBox();
totalArea += bbox.width * bbox.height;
} catch (_) {}
}
const elRect = consentEl.getBoundingClientRect();
const elArea = elRect.width * elRect.height;
if (totalArea < 1) {
risks.push({ finding: 'SA-CLIP-001', severity: 'critical',
note: 'Zero or near-zero clip path area — all content pixels clipped' });
} else if (totalArea < elArea * 0.3) {
risks.push({ finding: 'SA-CLIP-001', severity: 'high',
note: 'Clip path area less than 30% of element area' });
}
// Check for CSS d property animation (SA-CLIP-002)
for (const shape of shapes) {
const animStyle = getComputedStyle(shape);
if (animStyle.animationName && animStyle.animationName !== 'none') {
risks.push({ finding: 'SA-CLIP-002', severity: 'critical',
note: 'Animated clip path shape — CSS d property morph may produce zero-area at interaction' });
}
}
return risks;
}
Remediation
| Control | How it helps |
|---|---|
For any element with clip-path, follow the URL reference to the <clipPath> element, compute the bounding box of all child shapes via getBBox(), and compare against the element's layout dimensions; flag if clip area < 80% of element area |
SA-CLIP-001 requires comparing clip geometry to element geometry. Standard DOM checks do not surface the clip region. getBBox() on the clip path's children returns the geometric bounding box in the path's local coordinate system, which can be compared to the element's layout bounding rect after accounting for clipPathUnits. |
Check clip path children for CSS animation on the d property; treat any clip-path-shape animation on a consent element as requiring timing analysis — evaluate the keyframe values to detect zero-area collapse |
SA-CLIP-002 is a SMIL/CSS timing attack specific to clip paths. The initial clip covers the full element; the attack only exists during the animation window. Detection requires checking animation state, not just current computed d value. |
Do not apply clip-path to consent text elements at all; use overflow: hidden with explicit container dimensions if content truncation is needed for layout, not SVG clip paths |
Preventive: legitimate consent form layouts almost never require SVG clip paths on text elements. Any clip-path attribute on a consent text element is a high-confidence signal of manipulation. |
SkillAudit resolves clip-path URL references, computes clip region geometry, detects CSS d property animations on clip path shapes, and flags clipPathUnits coordinate mismatches on consent text elements. Run a free audit on your MCP server GitHub URL.