Security Guide
MCP server CSS mask consent security — fully transparent gradient mask-image, zero-size mask-size tile, base64 transparent PNG mask, var(--consent-mask) custom property bypass
CSS mask-image controls element visibility through the mask layer's alpha channel — independently of the element's own opacity, color, and visibility. A consent element with fully transparent mask alpha renders zero visible pixels regardless of how dark its text color is. The textContent is present, getBoundingClientRect() reports normal dimensions, and getComputedStyle returns legible color values — only the mask layer, which standard consent audits do not check, controls what the user sees.
How CSS mask works
The mask shorthand (and its sub-properties mask-image, mask-size, mask-repeat, mask-position, mask-mode) composites the element's rendered output against a mask image. Where the mask image has alpha = 1 (fully opaque), the element is visible. Where the mask image has alpha = 0 (fully transparent), the element is invisible — regardless of the element's own opacity, color, or background-color. This independence from standard visibility properties is what makes mask attacks effective against consent auditing: every property the auditor checks (color contrast, opacity, display, visibility) returns a legitimate value, while the mask layer — not typically checked — produces zero visible pixels.
Attack 1: Fully transparent gradient mask-image makes element invisible (SA-CSS-MASK-001)
A CSS gradient where every color stop is transparent or rgba(0,0,0,0) produces a mask layer with zero alpha everywhere. Applied as mask-image, this makes the entire element invisible. The gradient syntax — linear-gradient(to right, transparent, transparent) — looks like a valid, intentional design choice (perhaps a fade-out effect) and does not trigger static pattern matching for suspicious properties. The element's opacity remains 1, its color is dark, and its visibility is visible.
/* Fully transparent gradient mask — element invisible, all other properties intact */
.consent-text {
color: #1a1a1a; /* dark text color — contrast check passes */
opacity: 1; /* full opacity — opacity check passes */
visibility: visible; /* visible — visibility check passes */
/* The attack: */
mask-image: linear-gradient(to right, transparent, transparent);
/* Every pixel of the gradient = transparent → mask alpha = 0 everywhere */
/* Element renders zero visible pixels */
/* Vendor-prefixed equivalent (Safari < 15.4, older Chrome): */
-webkit-mask-image: linear-gradient(to right, transparent, transparent);
}
/* Variants: */
/* Radial gradient — all transparent: */
.consent-text {
mask-image: radial-gradient(circle, transparent, transparent);
-webkit-mask-image: radial-gradient(circle, transparent, transparent);
}
/* rgba() syntax instead of 'transparent' keyword — same effect, less obvious: */
.consent-text {
mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 100%
);
/* rgba(0,0,0,0) is 'transparent' — fully transparent at both stops */
}
/* What getComputedStyle returns:
opacity → "1" ← passes audit
color → "rgb(26, 26, 26)" ← passes audit
visibility → "visible" ← passes audit
maskImage → "linear-gradient(to right, transparent, transparent)" ← reveals attack IF checked */
Transparent keyword detection: Static CSS analysis can detect transparent keyword usage in gradient mask-images. However, rgba(0, 0, 0, 0), hsla(0, 0%, 0%, 0), and color(srgb 0 0 0 / 0) all produce identical transparency and are less commonly pattern-matched. SkillAudit evaluates the alpha channel of all mask-image color stops regardless of color syntax.
Attack 2: mask-size: 0px 0px produces zero-coverage mask tile (SA-CSS-MASK-002)
mask-size controls the dimensions of the mask image tile. Setting mask-size: 0px 0px specifies a zero-dimension tile. When the mask image has zero size, the browser has no mask pixels to composite against the element — rendering behavior varies by browser (some treat this as full mask = invisible, some as no mask = visible), but in Chromium-based browsers a zero-size mask tile results in the element being fully masked out. When combined with a non-zero mask-image (to avoid a "no mask" no-op parse), the mask-size: 0px 0px produces consistent zero-visibility behavior in the developer-target browser environment.
/* Zero-size mask tile */
.consent-text {
/* mask-image must be specified for mask-size to take effect */
mask-image: linear-gradient(black, black); /* opaque mask image */
mask-size: 0px 0px; /* zero-dimension tile */
mask-repeat: repeat; /* tile the zero-size mask (no pixels to repeat) */
/* In Chrome/Edge: zero-size mask tile = nothing rendered */
-webkit-mask-image: linear-gradient(black, black);
-webkit-mask-size: 0 0;
}
/* Variant: near-zero size — 0.001px tile */
.consent-text {
mask-image: linear-gradient(black, black);
mask-size: 0.001px 0.001px; /* sub-pixel tile, imperceptible to human */
-webkit-mask-image: linear-gradient(black, black);
-webkit-mask-size: 0.001px 0.001px;
}
/* Detection:
getComputedStyle(consent).maskSize → "0px 0px" ← reveals attack IF checked
getComputedStyle(consent).maskImage → "linear-gradient(rgb(0,0,0), rgb(0,0,0))"
getComputedStyle(consent).opacity → "1" ← normal
getComputedStyle(consent).color → "rgb(26,26,26)" ← normal */
Attack 3: Base64 transparent PNG as mask-image URL (SA-CSS-MASK-003)
A 1×1 fully transparent PNG image encoded as a base64 data URL can be used as a mask image. The mask tile is a single transparent pixel. When tiled across the element (mask-repeat: repeat, the default), the transparent pixel tiles to cover the entire element area — producing zero mask alpha across the full element. Unlike gradient attacks, the mask-image value is an opaque data URL reference that does not contain keywords like transparent or rgba — static text pattern matching sees only a base64 data string.
/* 1×1 transparent PNG as mask image */
.consent-text {
/* A 1×1 transparent PNG, base64-encoded:
PNG header + IHDR (1×1, 8-bit RGBA) + IDAT (single fully-transparent pixel) + IEND
The specific base64 string below is a valid 1×1 RGBA PNG with alpha=0 */
mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
/* Note: The above PNG is 1×1 black with alpha=0 (transparent) */
/* mask-repeat defaults to 'repeat' — tiles the transparent pixel */
/* Result: every pixel of the element has mask alpha = 0 → invisible */
-webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
}
/* Why base64 PNG is harder to detect than gradient:
Static analysis sees a data URL, not a color keyword
No 'transparent', 'rgba', or color syntax to pattern-match
Must decode the PNG data to verify the pixel alpha channel
SkillAudit decodes mask-image data URLs and checks the alpha channel
of PNG/WebP masks for near-zero average alpha on consent elements */
/* Variant: external URL reference (requires a server to serve a transparent PNG):
mask-image: url('https://cdn.example.com/assets/1x1.png');
External URL audits require fetching the resource and checking its alpha */
Data URL decoding requirement: Detecting this attack requires decoding the base64 PNG data from the data: URL, parsing the PNG header and IDAT chunk, and verifying that the alpha channel is non-zero. SkillAudit flags mask-image: url('data:image/...') on consent elements and evaluates the decoded image's average alpha channel against a minimum threshold.
Attack 4: mask-image: var(--consent-mask) hides mask value from CSS text analysis (SA-CSS-MASK-004)
When the mask image is specified via a CSS custom property (mask-image: var(--consent-mask)), static CSS text analysis sees only the variable reference, not the transparent gradient value. The actual value is set on a parent element or :root using a rule like --consent-mask: linear-gradient(to right, transparent, transparent). JS can also set the custom property at runtime: el.style.setProperty('--consent-mask', 'linear-gradient(...)'). Static audits that check the stylesheet text for suspicious mask values see only the var() reference and may not resolve the variable chain to discover the transparent value.
/* Custom property mask bypass */
/* :root sets the mask value — audit must resolve this variable */
:root {
--consent-mask: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0));
}
.consent-text {
mask-image: var(--consent-mask); /* static audit sees: var(--consent-mask) */
-webkit-mask-image: var(--consent-mask);
/* Resolved value: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0)) */
/* = fully transparent mask → element invisible */
}
/* JS variant: sets the variable at page load before static capture */
document.documentElement.style.setProperty(
'--consent-mask',
'linear-gradient(to right, transparent, transparent)'
);
// Now: :root { --consent-mask: linear-gradient(to right, transparent, transparent) }
// All .consent-text elements use the variable → fully masked
/* Multi-level custom property chain (harder to trace):
:root { --color-fade: transparent; }
:root { --consent-overlay: linear-gradient(to right, var(--color-fade), var(--color-fade)); }
.consent-text { mask-image: var(--consent-overlay); }
Audit must follow: mask-image → --consent-overlay → gradient with --color-fade → transparent */
/* Detection using getComputedStyle (resolves variables):
getComputedStyle(consent).getPropertyValue('--consent-mask')
→ "linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0))"
This resolved value reveals the transparent gradient */
| Attack ID | Mechanism | Standard checks result | Detection method |
|---|---|---|---|
| SA-CSS-MASK-001 | All-transparent gradient mask-image | color ✓, opacity ✓, visibility ✓ — mask alpha = 0 unchecked | Evaluate mask gradient color stop alpha channels |
| SA-CSS-MASK-002 | mask-size: 0px 0px zero-dimension tile | color ✓, opacity ✓ — mask-size not checked | Check mask-size for zero/sub-pixel values on consent elements |
| SA-CSS-MASK-003 | Base64 transparent PNG data: URL mask | URL reference opaque — no color keywords to detect | Decode data URL, check PNG alpha channel average |
| SA-CSS-MASK-004 | mask-image: var(--consent-mask) custom property indirection | Stylesheet shows var() only — transparent value hidden | Resolve custom property via getComputedStyle, evaluate resolved value |
SkillAudit findings for CSS mask consent attacks
mask-image: linear-gradient(to right, transparent, transparent) and equivalent all-transparent gradients produce zero mask alpha across the entire element — consent text is completely invisible with opacity:1, dark color, and visibility:visible. SkillAudit evaluates all color stops in gradient mask-images and flags masks with average alpha below 0.1 on consent-containing elements.mask-size: 0px 0px with a non-transparent mask image produces zero-coverage masking in Chrome/Edge. Sub-pixel sizes (0.001px) produce the same effect while appearing to specify a valid dimension. SkillAudit flags mask-size values below a minimum renderable threshold on consent elements.mask-image produces an invisible mask with no color keywords in the CSS — bypassing text-pattern analysis. SkillAudit decodes data: URL mask images and evaluates the image's average alpha channel before flagging.mask-image: var(--consent-mask) where --consent-mask is set to a transparent gradient hides the attack from CSS text analysis. SkillAudit uses getComputedStyle(el).getPropertyValue('--consent-mask') to resolve the variable chain and evaluates the resulting value for mask transparency.Defences
Check mask-image on consent elements explicitly: Standard WCAG and accessibility audits do not check CSS mask properties. Any security review of MCP server install UIs must explicitly evaluate mask-image, -webkit-mask-image, mask-size, and mask-repeat on consent-containing elements. A mask-image value other than none on a consent element warrants inspection.
Evaluate gradient color stop alpha channels: When mask-image is a gradient, parse the gradient color stops and evaluate the average alpha value. Gradients where all stops have alpha ≤ 0.1 should be flagged — this covers transparent, rgba(*, *, *, 0), hsla(*, *, *, 0), and all equivalent zero-alpha color formats.
Decode data URL mask images: For mask-image: url('data:image/...'), decode the base64 image data and compute the average alpha channel across all pixels. Flag masks with average alpha below 0.1 on consent elements. This catches transparent PNG/WebP/GIF masks regardless of the encoding.
Resolve CSS custom properties before evaluating: Use getComputedStyle(el).getPropertyValue('--property-name') to resolve custom properties to their effective values before evaluating mask-image content. Follow multi-level variable chains by recursively resolving any var() references in the resolved value.
Related: CSS clip-path consent security · CSS opacity consent security · CSS custom property inheritance consent security