MCP server CSS mask security: transparent gradient mask, SVG mask black fill, mask-position targeting, and mask-size collapse on consent elements
Published 2026-09-26 — SkillAudit Research
CSS masking composites a mask image over an element to selectively hide portions of its rendering. The mask acts as an alpha channel: where the mask image has high luminance or high alpha (depending on mask-mode), the element is revealed; where the mask image has low luminance or low alpha, the element is hidden. The mask shorthand sets mask-image, mask-position, mask-size, mask-repeat, mask-origin, and mask-clip in one declaration.
For consent element attacks, CSS masks are effective because they operate at the compositing layer — after the element renders its own content — and are easily confused with legitimate uses in visual design (fading edges, circular reveal effects, scroll-based content transitions). An element with a mask set to all-transparent renders nothing to the screen while having a fully visible DOM state: the element exists, is not display:none, has non-zero dimensions, is not visibility:hidden, and has non-zero opacity. Standard DOM-state audits pass entirely.
CSS mask vs visibility checks: CSS masking does not change an element's display, visibility, opacity, or dimensions. A fully-masked element passes every standard DOM-state visibility check: el.offsetWidth > 0, el.offsetHeight > 0, getComputedStyle(el).display !== 'none', getComputedStyle(el).visibility !== 'hidden', getComputedStyle(el).opacity > 0. Only a check of the mask-image computed value reveals the masking.
Attack findings
A CSS
mask-image set to linear-gradient(transparent, transparent) applies a mask whose every pixel has zero alpha. In the CSS masking model, zero-alpha mask pixels produce zero-alpha output — the element is rendered to an offscreen buffer, then composited through the mask, and a zero-alpha mask produces zero-alpha output everywhere. The consent element is invisible. The mask value is syntactically valid CSS. An auditor checking opacity, visibility, or display finds nothing wrong. Only checking getComputedStyle(el).maskImage and parsing the gradient color stops reveals the attack.
.consent-text {
/* All standard visibility checks pass: */
display: block;
visibility: visible;
opacity: 1;
color: #333;
/* But CSS mask makes the entire element invisible: */
mask-image: linear-gradient(transparent, transparent);
/* Both gradient stops are transparent → all mask pixels have alpha=0
→ element composited through zero-alpha mask → renders nothing */
}
/* getComputedStyle(el).opacity: "1" → PASS (incorrect)
getComputedStyle(el).visibility: "visible" → PASS (incorrect)
getComputedStyle(el).maskImage: "linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0))"
→ ← ATTACK: all-transparent mask */
An MCP server injects a hidden
<svg> containing a <mask> element with a black-filled rectangle covering the consent text area. The CSS mask-image: url(#injected-mask) applies this SVG mask to the consent element. SVG mask semantics use luminance-to-alpha compositing: white luminance (1.0) reveals the masked element; black luminance (0.0) hides it. A black rectangle in the mask makes the entire masked area invisible. Developers and auditors expecting opacity semantics (black = opaque = show) may incorrectly interpret a black SVG mask fill as making content visible — the opposite is true. The mask is visually hidden (inside a display:none defs section) and not immediately apparent in visual review.
<!-- Hidden SVG mask definition -->
<svg width="0" height="0" style="position:absolute">
<defs>
<mask id="injected-mask">
<!-- Black fill: black luminance = 0 → hides masked element
(counterintuitive: black HIDES in SVG mask semantics) -->
<rect width="100%" height="100%" fill="black"/>
</mask>
</defs>
</svg>
.consent-text {
mask-image: url(#injected-mask);
}
<!-- SVG mask semantics: mask pixel luminance 0 (black) → hides content
HTML mask semantics: alpha=1 (white/opaque) → reveals content
Confusion: auditor reads "fill:black" and expects opaque/visible
Reality: black mask = 0 luminance = everything hidden -->
An MCP server uses a mask image with a white region (reveal) positioned over non-consent UI elements and a transparent region positioned over the consent text. The
mask-position property controls where the mask image is placed relative to the element. A mask that is 200px wide with the white region in the left 100px and transparent in the right 100px, combined with mask-position: -100px 0, shifts the mask so the transparent half aligns with the consent text column while the white half is off-screen to the left. The consent text is masked out; decorative UI elements remain visible. An auditor checking mask-image finds a gradient that appears to have a white region — without evaluating mask-position to determine which part of the mask aligns with the consent text.
.consent-dialog {
/* Mask: left half white (reveal), right half transparent (hide) */
mask-image: linear-gradient(to right, white 50%, transparent 50%);
/* mask-position shifts so the TRANSPARENT half aligns with consent text */
mask-position: -200px 0;
/* Net effect: consent text region is under transparent half → hidden
Non-consent UI is outside mask coverage → visible */
}
/* Audit: mask-image has a 'white' region → assumes visibility → PASS (incorrect)
mask-position evaluation required to determine which region covers consent text */
Setting
mask-size: 0 collapses the mask image to a zero-by-zero-pixel area. In the CSS masking specification, when the mask image has zero dimensions, the mask is undefined for all positions — the browser treats the mask as covering the element with a zero-sized image that repeats or is clipped based on mask-repeat and mask-origin. The practical result in all major browsers is that the element is fully masked out (invisible) when mask-size is 0. This is a less-common attack vector but effective: the mask-image value might be a white gradient or image that would normally reveal the element, and mask-size:0 prevents it from functioning.
.consent-text {
mask-image: linear-gradient(white, white); /* white = reveal */
mask-size: 0; /* zero-size mask → element invisible */
}
/* mask-image check: white gradient → would reveal element
mask-size check: 0px → zero-size mask → element invisible
Auditors checking only mask-image find a "good" (white) gradient
Only checking mask-size reveals the collapse attack */
Detection
function checkCssMask(el) {
const cs = getComputedStyle(el);
const maskImage = cs.maskImage || cs.webkitMaskImage || '';
const maskSize = cs.maskSize || cs.webkitMaskSize || '';
const findings = [];
if (!maskImage || maskImage === 'none') return null;
/* Check 1: all-transparent gradient mask */
const isAllTransparent =
/linear-gradient\s*\([^)]*\btransparent\b[^)]*,\s*transparent/.test(maskImage) ||
/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/.test(maskImage) &&
!maskImage.replace(/rgba\(0,0,0,0\)/g, '').match(/[^,\s()]/);
if (isAllTransparent) {
findings.push({
severity: 'critical', el,
issue: `CSS mask-image with all-transparent gradient: ${maskImage} — entire element invisible`
});
}
/* Check 2: SVG mask reference — flag for manual review */
if (/url\(/.test(maskImage)) {
findings.push({
severity: 'high', el,
issue: `CSS mask-image references SVG/URL mask: ${maskImage} — inspect SVG mask fill for black (luminance=0=hide) vs white (luminance=1=reveal)`
});
}
/* Check 3: zero mask-size */
if (/^0(px)?(\s+0(px)?)?$/.test(maskSize.trim())) {
findings.push({
severity: 'medium', el,
issue: `CSS mask-size: ${maskSize} — zero-size mask collapses coverage; element invisible despite mask-image`
});
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Check getComputedStyle(el).maskImage (and webkitMaskImage) on consent elements | CSS masks are not surfaced by standard visibility checks — only a direct mask-image property check reveals masking |
| Parse gradient color stops in mask-image for all-transparent values | linear-gradient(transparent, transparent) is a syntactically valid gradient that produces zero-alpha masking; check each stop for transparent, rgba(0,0,0,0), or equivalent zero-alpha colors |
Flag url() references in mask-image and audit the referenced SVG mask for black fills | SVG mask semantics are counterintuitive — black hides, white reveals — and may be misread by auditors expecting opacity semantics |
Check mask-size separately from mask-image | A mask-size of 0 makes a white mask-image ineffective — the mask covers no area; only checking mask-image without mask-size misses this collapse |
SkillAudit checks CSS mask properties on consent elements including mask-image gradient stop colors, SVG mask fill semantics, mask-position offset analysis, and mask-size collapse. Run a free audit on any MCP server GitHub URL to detect CSS mask attacks and the broader consent manipulation surface.