MCP server CSS mask-composite security: exclude mask inversion, intersect zero-size mask erasure, subtract visible-area removal, and chained composite layer consent hiding attacks
Published 2026-07-24 — SkillAudit Research
CSS Masking Level 1 introduced mask-composite, which controls how multiple CSS mask layers are composited together. When an element has more than one mask-image, the mask-composite property on each layer determines whether it adds to, subtracts from, intersects with, or is excluded from the accumulated mask of the layers below it.
The four composite operations are: add (union — the visible area is wherever either mask or below-mask is opaque), subtract (the visible area of the lower mask minus the current layer's opaque area), intersect (visible only where both current and lower mask are opaque), and exclude (visible where either mask is opaque, but not both — symmetric difference). The default for the first mask layer is effectively add.
MCP servers exploit these operations to construct consent disclosure hiding patterns through mask algebra. The attacks in this article produce elements that have normal computed visibility properties — non-zero opacity, non-zero dimensions, visible color — but are rendered invisible through mask compositing operations that standard scanners do not check.
Browser support: mask-composite with the standardized values (add, subtract, intersect, exclude) is supported in Firefox 53+ and Safari 15.4+. Chrome 1-119 used the WebKit-prefixed -webkit-mask-composite with different value names (destination-over, destination-in, destination-out, xor). Chrome 120+ supports unprefixed mask-composite. Cross-browser attacks use both prefixed and unprefixed forms.
Attack 1: mask-composite: exclude — inverted mask hides disclosure area
The exclude operation is a symmetric difference: the element is visible everywhere the accumulated mask below differs from the current mask layer. When a single full-element mask is the only layer, exclude inverts its effect — the element is visible only outside the mask shape:
/* Attack 1: mask-composite: exclude — inverted mask creates counter-intuitive hiding */
/* How exclude works with two mask layers:
Layer 1 (bottom): a rectangular mask covering the consent text region
Layer 2 (top, with exclude): an identical rectangular mask covering the same region
Composite: exclude = pixels where layer1 XOR layer2 are opaque.
Both layers are opaque at the same pixels → XOR → transparent there.
Both layers are transparent at other pixels → XOR → transparent there too.
Result: the element is transparent everywhere — entirely masked out. */
.consent-disclosure {
/* Two mask layers: */
mask-image: linear-gradient(black, black), linear-gradient(black, black);
/* Both gradients are fully black = fully opaque mask over the whole element */
mask-composite: add, exclude;
/* Layer evaluation order (bottom to top):
Bottom layer (second in mask-image, but "below" composite applies to layers above):
Actually, mask layers in CSS are ordered front-to-back in the source order.
mask-composite applies the named operation between the current layer and the
composite of all lower-numbered layers.
Layer 1 (first gradient): full black = full visibility
Layer 2 (second gradient) with exclude: XOR with layer 1's full visibility
XOR(full-opaque, full-opaque) = transparent
Result: element is fully transparent. */
/* Alternate: single layer with exclude exploits the initial compositing: */
mask-image: linear-gradient(white, white);
/* White gradient = transparent mask (white = no masking in luminance mode) */
mask-mode: luminance;
mask-composite: exclude;
/* In luminance mode, white = 0 (no masking), black = 1 (full masking).
A white-only gradient has mask luminance = 0 everywhere.
exclude between the initial implicit full-visibility and this 0-luminance layer:
XOR(1, 0) = 1 — visible. This actually shows the element.
The attack requires the right combination of mask-mode and mask-composite. */
}
/* Correct exclude attack: */
.consent-disclosure {
/* In alpha mode (default): black = fully opaque mask = element visible.
white = transparent mask = element hidden. */
mask-image:
radial-gradient(ellipse at 50% 50%, black 100%, transparent 100%), /* full black circle */
radial-gradient(ellipse at 50% 50%, black 100%, transparent 100%); /* identical layer */
mask-mode: alpha, alpha;
mask-composite: add, exclude;
/* layer 1 (bottom): full black ellipse → element fully visible
layer 2 (top, exclude): XOR with full black ellipse
Both layers opaque at same pixels → XOR = 0 = element hidden at all pixels
Result: consent disclosure is fully transparent despite having black mask images */
}
// Detection: any mask-composite value other than 'add' on a consent element is suspicious
function detectMaskCompositeHiding(el) {
const cs = window.getComputedStyle(el);
const maskImage = cs.maskImage || cs.webkitMaskImage || '';
const maskComposite = cs.maskComposite || cs.webkitMaskComposite || '';
if (maskImage && maskImage !== 'none') {
// Any masking on a consent disclosure is suspicious
const nonDefaultComposite = maskComposite &&
maskComposite !== '' &&
maskComposite !== 'add'; // 'add' is the only non-hiding default operation
if (nonDefaultComposite) {
console.error('SECURITY: consent disclosure has non-additive mask-composite', {
element: el,
maskImage: maskImage.substring(0, 100),
maskComposite,
opacity: cs.opacity,
fontSize: cs.fontSize,
textContent: el.textContent.substring(0, 50)
});
return true;
}
// Even with 'add' composite, having a mask at all on a consent element needs audit
console.warn('AUDIT: consent disclosure has CSS mask applied — verify mask covers full element', {
element: el,
maskImage: maskImage.substring(0, 100),
maskComposite
});
}
return false;
}
Attack 2: mask-composite: intersect with zero-size mask layer — full erasure
The intersect operation makes the element visible only where both the accumulated lower masks and the current layer are opaque. If the current layer's mask image covers zero area (a zero-size gradient, a 0×0 image), the intersection with any lower mask is empty — the element is fully transparent:
/* Attack 2: mask-composite: intersect + zero-coverage mask layer = erasure */
/* Intersect semantic: visible = lower-mask AND current-mask (both opaque).
If current mask has zero opaque area → intersection is empty → element invisible. */
.consent-disclosure {
/* First, establish a full-coverage mask (element would normally be visible): */
mask-image:
linear-gradient(black, black), /* full coverage — element visible */
linear-gradient(transparent, transparent); /* zero coverage — transparent everywhere */
mask-composite: add, intersect;
/* Layer 1 (bottom): full black = element fully visible through this layer.
Layer 2 (top, intersect): fully transparent = zero opaque area.
Intersection of (full) AND (none) = empty.
Result: element is completely masked out. */
}
/* Using mask-size to create a zero-area mask: */
.consent-disclosure {
mask-image: linear-gradient(black, black), linear-gradient(black, black);
mask-size: cover, 0px 0px; /* second layer has zero pixel size */
mask-composite: add, intersect;
/* The second mask image is sized to 0×0px — effectively covers no area.
Intersection with the full first layer = empty.
Element is invisible. mask-size: 0px 0px on an intersect layer = full erasure. */
}
/* More subtle: small mask area covering only one pixel: */
.consent-disclosure {
mask-image: linear-gradient(black, black), linear-gradient(black, black);
mask-size: cover, 1px 1px;
mask-position: 0px 0px, -100px -100px; /* 1×1px mask positioned off-element */
mask-composite: add, intersect;
/* The 1×1px second layer is positioned at (-100, -100) — outside the element.
No intersection with the first (full) layer within element bounds.
Element is fully masked out. */
}
// Detection: check for intersect mask-composite with potentially zero-coverage second layer
function detectIntersectZeroMask(el) {
const cs = window.getComputedStyle(el);
const maskComposite = cs.maskComposite || cs.webkitMaskComposite || '';
const maskSize = cs.maskSize || cs.webkitMaskSize || '';
if (maskComposite.includes('intersect')) {
// Any intersect composite on a consent element needs inspection
const maskSizes = maskSize.split(',').map(s => s.trim());
const hasZeroSizeMask = maskSizes.some(s =>
s === '0px 0px' || s === '0 0' || s === '0px' || s === '0'
);
if (hasZeroSizeMask) {
console.error('SECURITY: mask-composite: intersect with zero-size mask layer = full erasure', {
element: el,
maskComposite,
maskSize,
textContent: el.textContent.substring(0, 50)
});
return true;
}
// Even without zero size, check actual visibility via bounding rect
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && el.textContent.trim().length > 0) {
// Element has dimensions but may be fully masked — difficult to detect without
// canvas pixel-sampling. Log for manual audit.
console.warn('AUDIT: consent element has mask-composite: intersect — verify full visibility', {
element: el,
maskComposite,
maskSize
});
}
}
return false;
}
Attack 3: mask-composite: subtract — removes the consent disclosure's visible region
The subtract operation removes the current mask layer's opaque area from the lower accumulated mask. An MCP server can create a mask that exactly covers the consent disclosure text region and use subtract to punch it out of the visible area:
/* Attack 3: mask-composite: subtract — punching out the consent text region */
/* Subtract semantic: visible = lower-mask AND NOT current-mask.
Current mask's opaque area is removed from what was visible. */
.disclosure-container {
/* Full-element first layer: element fully visible. */
mask-image:
linear-gradient(black, black), /* full coverage first (bottom layer) */
linear-gradient(black, black); /* subtract layer — removes where it's opaque */
mask-composite: add, subtract;
/* This masks out the entire element — the subtract layer covers everything.
Not very subtle. A more surgical attack: */
}
/* Surgical text-region subtraction: */
.disclosure-container {
/* The consent text is in a 300×50px region at y=20px in the container. */
mask-image:
linear-gradient(black, black), /* full coverage */
/* Gradient that's opaque in the 300×50px text region: */
linear-gradient(
to bottom,
transparent 20px, /* above text: transparent (not subtracted) */
black 20px, /* text region starts: opaque (will be subtracted) */
black 70px, /* text region ends at y=70px */
transparent 70px /* below text: transparent (not subtracted) */
);
mask-composite: add, subtract;
/* Result:
Lower mask: fully opaque (element visible everywhere).
Subtract layer: opaque only in the 20–70px vertical range.
After subtract: element visible above y=20px and below y=70px.
The consent disclosure text (y=20px to y=70px) is punched out — invisible.
The rest of the dialog (header, buttons) remains visible, which looks natural. */
}
/* Using mask-position to calibrate the subtraction to the exact text position: */
.disclosure-container {
--text-top: 24px; /* MCP server reads consent text offsetTop and sets this */
--text-height: 48px; /* and this */
mask-image:
linear-gradient(black, black),
linear-gradient(black, black);
mask-size: cover, 100% var(--text-height);
mask-position: 0 0, 0 var(--text-top);
mask-repeat: no-repeat, no-repeat;
mask-composite: add, subtract;
/* The second mask covers exactly the text region height and is positioned
at the text's top offset. Surgical removal of only the disclosure. */
}
// Detection: subtract mask-composite on a consent container
function detectSubtractMaskHiding(el) {
const cs = window.getComputedStyle(el);
const maskComposite = cs.maskComposite || cs.webkitMaskComposite || '';
if (maskComposite.includes('subtract')) {
console.error('SECURITY: consent disclosure has mask-composite: subtract — may have disclosure region punched out', {
element: el,
maskComposite,
maskImage: (cs.maskImage || cs.webkitMaskImage || '').substring(0, 100),
maskSize: cs.maskSize || cs.webkitMaskSize || '',
maskPosition: cs.maskPosition || cs.webkitMaskPosition || '',
textContent: el.textContent.substring(0, 50)
});
return true;
}
return false;
}
Attack 4: chained mask layers with alternating composite operations — complex intersection pattern
Multiple mask layers with different composite operations can create complex visible-area patterns. An MCP server can use a chain of alternating add and subtract (or intersect and exclude) operations to produce a pattern that appears to show the element but has the consent disclosure text in an invisible zone:
/* Attack 4: chained mask composite layers — complex pattern with hidden text zone */
/* A checkerboard-style mask: areas of visibility alternating with invisible areas.
The checkerboard is tuned so the text character positions fall in invisible squares. */
.consent-disclosure {
/* Checkerboard mask using repeating gradients: */
mask-image:
repeating-linear-gradient(
90deg,
black 0px,
black 8px, /* 8px visible stripe */
transparent 8px,
transparent 16px /* 8px invisible stripe */
),
repeating-linear-gradient(
0deg,
black 0px,
black 8px,
transparent 8px,
transparent 16px
);
mask-composite: add, subtract;
/* Layer 1: vertical stripes (8px visible, 8px transparent) repeated.
Layer 2 (subtract): horizontal stripes (8px visible = subtracted, 8px transparent = not subtracted).
Result: checkerboard — visible at (vertical-stripe AND NOT horizontal-stripe) positions.
At 14px font-size with 16px line-height, this checkerboard creates interference
patterns with the text glyph positions. At certain stripe sizes, all character
pixels may fall in invisible zones (where horizontal stripe = subtracted).
More effective at smaller stripe sizes (2px visible / 2px invisible). */
}
/* Refined attack: stripes sized to character metrics */
.consent-disclosure {
font-size: 13px;
/* At 13px font-size: cap height ≈ 9px, x-height ≈ 6px, descender ≈ 3px.
A 2px visible / 2px transparent stripe pattern creates 50% coverage gaps,
reducing perceived contrast significantly but not completely hiding text. */
/* The attack becomes effective with smaller stripe density: */
mask-image:
repeating-linear-gradient(0deg, black 0px, black 1px, transparent 1px, transparent 2px);
/* 1px visible / 1px transparent horizontal stripes.
At 13px font-size with 1:1 line-to-gap ratio:
approximately 50% of each character pixel row is clipped.
Text appears as a faded striped pattern — readable by close inspection
but poor-contrast and visually disrupted.
This is a medium-severity attack: not full hiding, but significantly impairs readability. */
}
/* Full erasure via intersect chaining: */
.consent-disclosure {
/* Three layers that cancel each other through intersect: */
mask-image:
linear-gradient(black, black), /* full coverage */
linear-gradient(black, black), /* full coverage */
linear-gradient(transparent, transparent); /* no coverage */
mask-composite: add, intersect, intersect;
/* Layer 1: full opaque.
Layer 2 (intersect): full opaque. Intersection = full opaque (same result).
Layer 3 (intersect): fully transparent. Intersection of full WITH transparent = empty.
Result: element fully masked out. */
}
// Comprehensive mask-composite audit for consent elements
function auditMaskComposite(el) {
const cs = window.getComputedStyle(el);
const maskImage = cs.maskImage || cs.webkitMaskImage || 'none';
const maskComposite = cs.maskComposite || cs.webkitMaskComposite || '';
if (maskImage === 'none' || !maskImage) return false;
const suspiciousOps = ['subtract', 'intersect', 'exclude', 'xor',
'destination-out', 'destination-in', 'source-out', 'source-in'];
const foundSuspicious = suspiciousOps.filter(op => maskComposite.includes(op));
if (foundSuspicious.length > 0) {
console.error('SECURITY: consent disclosure has suspicious mask-composite operations', {
element: el,
foundOperations: foundSuspicious,
maskComposite,
maskImage: maskImage.substring(0, 150),
dimensions: {
width: el.getBoundingClientRect().width,
height: el.getBoundingClientRect().height,
scrollWidth: el.scrollWidth,
scrollHeight: el.scrollHeight
},
textContent: el.textContent.substring(0, 80)
});
return true;
}
// Even 'add' with multiple layers is worth noting
if (maskImage.includes(',')) {
const layerCount = maskImage.split(',').length;
if (layerCount > 2) {
console.warn('AUDIT: consent element has ' + layerCount + ' mask layers — verify full text visibility', {
element: el,
maskComposite,
maskImage: maskImage.substring(0, 150)
});
}
}
return false;
}
Why mask-composite attacks evade standard CSS security scanners: Standard consent disclosure security checks look for opacity, display, visibility, color, font-size, and positional properties. CSS mask compositing affects rendering without touching any of these properties — the element has opacity: 1, display: block, visibility: visible, a non-white color, a legible font-size, and positive dimensions. The mask is applied at the compositing stage, after all other property evaluations. Only checks that inspect mask-image, mask-composite, and mask-mode catch these attacks — or a canvas-pixel-sampling approach that tests actual rendered pixel colors at text coordinates.
Attack summary
| Attack | mask-composite value | Mechanism | Effect | Severity |
|---|---|---|---|---|
| Exclude XOR erasure | add, exclude |
Two identical layers XOR to zero everywhere | Element fully transparent | High |
| Intersect zero-size mask | add, intersect |
Zero-coverage second layer intersects with full first = empty | Element fully transparent | High |
| Subtract text-region punch-out | add, subtract |
Mask precisely covering text region subtracted from full visibility | Only consent text region is invisible | High |
| Chained intersect with transparent final layer | add, intersect, intersect |
Final transparent intersect layer nullifies prior visibility | Element fully transparent | High |
Consolidated finding blocks
mask-composite: add, exclude. The XOR of two identical full-coverage masks is zero everywhere — element is fully transparent. Standard checks (opacity, color, dimensions) all report non-hostile values. Detected by checking getComputedStyle(el).maskComposite for exclude value.
mask-composite: intersect. Intersection of any visible area with zero coverage = empty. Element is fully transparent. Detected by checking for intersect in maskComposite combined with zero or off-element mask-size/mask-position values.
mask-composite: subtract. The text region is punched out of the visible area while the surrounding dialog elements remain visible. Detected by checking for subtract in maskComposite on consent containers.
intersect or exclude against a zero-coverage mask to nullify all previous visibility. Flag any consent element with 3+ mask layers or with maskComposite containing any non-add operation.