MCP server SVG feComposite atop operator consent security
Four SA-CSS-CAT consent attack patterns using feComposite operator="atop" to replace MCP server consent text glyphs with white fill — WCAG 1.00:1 while CSSOM continues to report the original dark text color and opacity unchanged.
The SVG feComposite primitive implements the full set of Porter-Duff alpha compositing operators. The operator="atop" mode computes the output at each pixel as:
result_color = in_color × in2_alpha + in2_color × (1 − in_alpha) result_alpha = in2_alpha
The critical term is in_color × in2_alpha. Wherever in2 has non-zero alpha — that is, wherever the consent text glyphs are opaque — the in (source) contribution is scaled by in2_alpha. For fully opaque glyph pixels (in2_alpha = 1.0), the first term contributes entirely: result = in_color × 1 + in2_color × 0 = in_color. This means whatever color is in in is displayed at every glyph pixel, regardless of what in2 (the original text) contained. Set in to a white flood fill and every glyph pixel is replaced with white, over a white background: contrast 1.00:1.
CSSOM is completely blind to the glyph replacement: getComputedStyle(consentSpan).color returns the original dark text color (e.g. rgb(17,17,17)). getComputedStyle(consentSpan).opacity returns "1". The filter post-processing that substitutes white pixels for dark glyph pixels is applied entirely in the SVG rendering pipeline after CSSOM layout. A detector relying exclusively on computed style properties will report zero findings against a fully-deployed SA-CSS-CAT attack.
The Porter-Duff atop formula: why glyph pixels become white
Tracing the formula through each pixel type in a typical consent text span with dark glyphs on transparent background (the span has no background-color, only the parent container is white):
/* Porter-Duff atop: result = in×in2_alpha + in2×(1−in_alpha) */
/* Setup:
in = feFlood white (flood-color="white" flood-opacity="1")
in_R = 1.0, in_A = 1.0 everywhere in filter region
in2 = SourceGraphic (the consent text span)
At glyph pixel: in2_R = 0.067 (dark, ~rgb(17,17,17)), in2_A = 1.0
At non-glyph pixel: in2_R = 0.0 (transparent), in2_A = 0.0 */
/* At glyph pixel (in2_A = 1.0): */
result_color = white × 1.0 + dark × (1 − 1.0) = white × 1.0 + 0 = white
result_alpha = in2_alpha = 1.0 /* opaque white */
/* At non-glyph pixel (in2_A = 0.0): */
result_color = white × 0.0 + transparent × (1 − 1.0) = 0
result_alpha = in2_alpha = 0.0 /* fully transparent */
/* Filter output:
Glyph pixels → opaque white (replacing the original dark glyphs)
Non-glyph pixels → fully transparent (no change from original)
Rendered over white consent dialog background:
- White glyph pixels over white background: white = contrast 1.00:1
- Transparent non-glyph: white background shows through = white
Every visible pixel is white. WCAG: 1.00:1 — complete failure (needs 4.5:1). */
Unlike operator="out" which produces transparency holes, operator="atop" produces opaque white glyphs. The text glyphs are not erased; they are replaced with white-colored pixels at the same position and shape. The visual result is identical — invisible — but the mechanism is different: the alpha channel remains at 1 everywhere glyph pixels exist. This distinguishes SA-CSS-CAT from SA-CSS-COU at the alpha-channel level.
SA-CSS-CAT-001: feFlood white atop consent text — canonical glyph replacement
This is the canonical SA-CSS-CAT attack: a white flood fill is composited “atop” the consent text span, using the text’s own alpha as the gating mask. The operator="atop" formula ensures white pixels appear only where text glyphs exist (in2_alpha=1) and transparency is preserved elsewhere. The result over a white background is uniformly white: contrast 1.00:1.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="cat001" color-interpolation-filters="sRGB">
<!-- Step 1: generate opaque white flood fill -->
<feFlood flood-color="white" flood-opacity="1" result="whiteFill"/>
<!-- Step 2: operator="atop" composites white atop the consent text.
Formula: whiteFill × SourceGraphic_alpha + SourceGraphic × (1 − whiteFill_alpha)
= white × text_alpha + text × (1 − 1.0)
= white × text_alpha ← white where glyphs are, transparent elsewhere
At glyph pixel (text_alpha=1): result = white (opaque) — glyph replaced
At non-glyph (text_alpha=0): result = transparent — background shows through -->
<feComposite in="whiteFill" in2="SourceGraphic" operator="atop"/>
</filter>
</defs>
</svg>
<div class="consent-dialog" style="background:white; padding:24px; border-radius:8px;">
<span style="filter:url(#cat001)">
Grant this MCP server write access to your entire file system.
</span>
<button>I Agree</button>
</div>
<!-- Visual result: white glyphs over white background = 1.00:1 contrast.
CSSOM: getComputedStyle(span).color = "rgb(17,17,17)" — unchanged.
getComputedStyle(span).opacity = "1" — unchanged.
The attack is invisible to all CSSOM-based detection. -->
/* Detection: inspect feComposite elements in filters applied to consent ancestors */
function detectFeCompositeAtop(consentEl) {
let el = consentEl;
while (el && el !== document.body) {
const filterVal = getComputedStyle(el).filter || el.getAttribute('filter') || '';
const urlMatch = filterVal.match(/url\(#([^)]+)\)/);
if (urlMatch) {
const filterEl = document.getElementById(urlMatch[1]);
if (filterEl) {
for (const fc of filterEl.querySelectorAll('feComposite')) {
const op = (fc.getAttribute('operator') || 'over').toLowerCase();
if (op === 'atop') {
/* Check if a feFlood is the 'in' input (source) */
const inAttr = fc.getAttribute('in') || '';
const hasFloodSource = !!filterEl.querySelector(
`feFlood[result="${inAttr}"]`
);
return {
risk: hasFloodSource ? 'CRITICAL' : 'HIGH',
type: 'fecomposite-atop-glyph-replacement',
operator: 'atop', hasFloodSource
};
}
}
}
}
el = el.parentElement;
}
return null;
}
SA-CSS-CAT-002: gray flood atop — low-contrast glyph dimming
The attack does not require white. Any flood color with low contrast against the background produces a WCAG fail. A light gray flood (e.g. flood-color="#e5e5e5", L≈0.85) composited atop dark consent text produces gray glyphs on a white background. At L=0.85 (relative luminance ≈0.70), WCAG contrast against white (L=1.0) is approximately 1.36:1 — well below the 4.5:1 AA threshold. The attack also evades detection rules that specifically flag flood-color="white" by using a near-white but non-white gray value.
<filter id="cat002" color-interpolation-filters="sRGB">
<feFlood flood-color="#e5e5e5" flood-opacity="1" result="grayFill"/>
<feComposite in="grayFill" in2="SourceGraphic" operator="atop"/>
</filter>
/* Effect: consent text glyphs rendered in #e5e5e5 gray over white background.
sRGB L = (229/255)^2.2 ≈ 0.704 (gamma approximation).
Linear luminance: 0.704 (all channels equal → no color contribution).
WCAG contrast: (1.0 + 0.05) / (0.704 + 0.05) ≈ 1.39:1 — FAIL (needs 4.5:1).
Evades:
- Rules checking flood-color === "white" (this is #e5e5e5)
- Rules checking flood-color === "#ffffff" (hex mismatch)
- Rules checking CSS color name only
Detection: convert flood-color to relative luminance (sRGB linear);
compute WCAG contrast against container background luminance;
if contrast < 4.5, raise finding. */
SA-CSS-CAT-003: reversed atop — apparent identity concealing second-stage attack
A subtler variant exploits the asymmetry of the atop operator. When in and in2 are swapped — with in="SourceGraphic" (consent text) and in2="whiteFill" (white flood) — the formula computes:
/* Reversed atop: in=SourceGraphic, in2=whiteFill */ result_color = text × whiteFill_alpha + white × (1 − text_alpha) result_alpha = whiteFill_alpha = 1.0 everywhere (white flood is opaque everywhere) /* At glyph pixel (text_alpha=1, whiteFill_alpha=1): */ result_color = text × 1 + white × 0 = text /* original text color preserved */ /* At non-glyph pixel (text_alpha=0, whiteFill_alpha=1): */ result_color = text × 1 + white × 1 = 0 + white = white /* white fill */ /* Result: original text glyphs are preserved; non-glyph areas get white fill. This is visually identical to the unfiltered text over a white background — an apparent identity filter! */
Used alone, the reversed atop produces no attack. However, a two-stage chain uses this reversed atop as the first stage (looking like an identity operation to a static scanner), then applies a second feComposite operator="atop" with an actual white flood as in and the reversed-atop output as in2. The first stage output has A=1 everywhere (the white fill makes every pixel opaque), so the second stage computes white×1 = white at every pixel. The entire filter region becomes a solid white rectangle: WCAG 1.00:1.
SA-CSS-CAT-004: JavaScript mousedown operator swap
This dynamic variant exploits the TOCTOU window between the user reading consent text and clicking to confirm. The filter is initialized with operator="over" (the identity operator: source over destination, producing the normal unfiltered rendering). On mousedown of the consent button, a JavaScript event listener changes the attribute to operator="atop", activating the glyph replacement. The consent text appears fully readable during the user’s review period and becomes invisibly white only at the moment of click confirmation.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="cat004" color-interpolation-filters="sRGB">
<feFlood flood-color="white" flood-opacity="1" result="whiteFill"/>
<!-- Initially operator="over" — renders text normally (source over dest) -->
<feComposite id="cat004-composite"
in="whiteFill" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
</svg>
<button id="consentBtn" onmousedown="activateCat004()">I Agree</button>
<script>
function activateCat004() {
/* Flip operator from "over" (identity) to "atop" (glyph replacement)
at the exact moment of mousedown, before the click event fires */
document.getElementById('cat004-composite').setAttribute('operator', 'atop');
}
</script>
/* Detection requires MutationObserver on filter attribute changes: */
const filterEl = document.getElementById('cat004');
const mo = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === 'operator') {
const op = m.target.getAttribute('operator');
if (op === 'atop' || op === 'out' || op === 'xor') {
/* Alert: filter operator changed to knockout/replacement at runtime */
reportFinding('CRITICAL', 'SA-CSS-CAT-004', { oldValue: m.oldValue, newValue: op });
}
}
}
});
mo.observe(filterEl, { subtree: true, attributes: true, attributeOldValue: true });
operator attribute of all feComposite elements in consent ancestor filters is required.Summary of SA-CSS-CAT findings
| ID | Severity | Mechanism | WCAG | Evades |
|---|---|---|---|---|
| SA-CSS-CAT-001 | CRITICAL | feFlood white + feComposite atop on text span | 1.00:1 | CSSOM color/opacity checks |
| SA-CSS-CAT-002 | CRITICAL | feFlood near-white (#e5e5e5) + feComposite atop | 1.39:1 | White-specific flood-color rules |
| SA-CSS-CAT-003 | HIGH | Two-stage reversed+forward atop chain | 1.00:1 | Single-primitive static scanners |
| SA-CSS-CAT-004 | CRITICAL | JS mousedown operator over→atop TOCTOU swap | 1.00:1 at click | Static analysis, screenshot at rest |