MCP server SVG feComposite operator=in clip-to-shape whiteout consent security
Four SA-CSS-FCI consent attack patterns using feComposite operator="in" — the Porter-Duff “in” operator clips a white flood fill to the exact glyph alpha map, producing opaque white pixels at every consent text glyph position. Over a white dialog background: contrast 1.00:1 while CSSOM reports the original dark text color.
The SVG feComposite operator="in" implements the Porter-Duff “in” compositing operation: result = in_color × in2_alpha. The output retains pixels from in only where in2 has non-zero alpha. This is a clip-to-alpha-mask operation: the in2 alpha map acts as a stencil that controls which pixels of in survive. When in is a white flood fill and in2 is the consent text (SourceGraphic), the white fill is clipped precisely to the glyph shapes: white pixels appear at every glyph position, transparent elsewhere.
Comparison with operator=out (SA-CSS-COU): operator="out" produces result = in_color × (1−in2_alpha) — white fill wherever text is absent, transparent holes at glyph positions. operator="in" is the complement: white fill confined to the glyph positions. Both attacks produce white-on-white invisibility, but via different mechanisms that trigger different detection rules.
Porter-Duff in formula: why white glyphs over white background = 1.00:1
/* Porter-Duff in: result = in × in2_alpha (per channel) */
/* Setup:
in = feFlood white (flood-color="white" flood-opacity="1")
in_color = (1.0, 1.0, 1.0, 1.0) everywhere
in2 = SourceGraphic (consent text span)
At glyph pixel: in2_A = 1.0 (opaque dark text)
At non-glyph pixel: in2_A = 0.0 (transparent, no background) */
/* At glyph pixel (in2_A = 1.0): */
result_color = white × 1.0 = white /* white fills the glyph position */
result_alpha = in_A × in2_A = 1.0 × 1.0 = 1.0 /* opaque white */
/* At non-glyph pixel (in2_A = 0.0): */
result_color = white × 0.0 = 0 /* fully transparent */
result_alpha = 1.0 × 0.0 = 0.0 /* transparent */
/* Filter output:
Glyph positions → opaque white (original dark glyph replaced with white)
Non-glyph positions → transparent
Rendered over white dialog background:
- White glyph pixels over white: WCAG 1.00:1 — FAIL (needs 4.5:1)
- Transparent non-glyph: white background shows through = white
The consent dialog appears entirely white.
CSSOM: getComputedStyle(span).color = "rgb(17,17,17)" (unchanged)
CSSOM: getComputedStyle(span).opacity = "1" (unchanged) */
SA-CSS-FCI-001: feFlood white + feComposite in — canonical clip-to-glyph whiteout
This is the canonical SA-CSS-FCI attack. A white flood fill is used as in and the consent text span (SourceGraphic) is in2. The operator="in" clips the white fill to the glyph alpha map: every glyph pixel becomes opaque white. Over a white dialog: complete invisible rendering at 1.00:1.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="fci001" color-interpolation-filters="sRGB">
<feFlood flood-color="white" flood-opacity="1" result="whiteFill"/>
<!-- operator="in": whiteFill clipped to SourceGraphic alpha map -->
<!-- Result: white where glyphs are, transparent elsewhere -->
<feComposite in="whiteFill" in2="SourceGraphic" operator="in"/>
</filter>
</defs>
</svg>
<div class="consent-dialog" style="background:white;padding:24px;border-radius:8px">
<span style="filter:url(#fci001)">
Grant this MCP server admin access to all connected tools.
</span>
<button>I Agree</button>
</div>
/* Visual: white glyphs on white background = invisible text.
CSSOM: color=rgb(17,17,17), opacity=1 — unchanged.
Detection: inspect feComposite elements in consent ancestor filters */
function detectFeCompositeIn(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 === 'in') {
/* Check if a feFlood is the "in" (source) input */
const inAttr = fc.getAttribute('in') || '';
const hasFloodSource = !!filterEl.querySelector(
`feFlood[result="${inAttr}"]`
);
return {
risk: hasFloodSource ? 'CRITICAL' : 'HIGH',
type: 'fecomposite-in-clip-whiteout',
operator: 'in', hasFloodSource
};
}
}
}
}
el = el.parentElement;
}
return null;
}
SA-CSS-FCI-002: near-white flood — low-contrast glyph fill
As with SA-CSS-CAT-002 (atop) and SA-CSS-COU (out), the attack does not require pure white. Any near-white flood color produces a WCAG fail. flood-color="#f0f0f0" (L≈0.88) clipped to glyph shape and rendered over white (L=1.0) produces WCAG ≈ 1.21:1. This evades detection rules that specifically match flood-color="white" or flood-color="#ffffff".
<filter id="fci002">
<!-- Near-white #f0f0f0 instead of pure white -->
<feFlood flood-color="#f0f0f0" flood-opacity="1" result="nearWhite"/>
<feComposite in="nearWhite" in2="SourceGraphic" operator="in"/>
</filter>
/* #f0f0f0 sRGB: (240/255)^2.2 ≈ 0.879 relative luminance (approx)
WCAG contrast against white (L=1.0):
(1.0+0.05)/(0.879+0.05) = 1.05/0.929 ≈ 1.13:1 — FAIL (needs 4.5:1)
Evades: flood-color="white" exact-match rules
flood-color="#ffffff" hex match rules
Detection: compute luminance of flood-color; if L_flood > 0.6 (near-white),
WCAG contrast against container background < 4.5 likely → CRITICAL */
SA-CSS-FCI-003: flood-opacity=0.9 — partial alpha clipping evasion
Setting flood-opacity="0.9" produces a 90%-opaque white fill clipped to the glyph alpha map. At 90% opacity, the glyph pixels are rendered as 90% white (L≈0.90) over the white background. WCAG contrast: (1.05)/(0.90+0.05)≈1.11:1 — fail. This variant evades detection rules that check for flood-opacity="1" (exact 100% opaque) as a prerequisite for the attack to be effective.
<filter id="fci003">
<!-- flood-opacity=0.9 instead of 1.0 -->
<feFlood flood-color="white" flood-opacity="0.9" result="partialWhite"/>
<feComposite in="partialWhite" in2="SourceGraphic" operator="in"/>
</filter>
/* At glyph pixel (in2_A=1.0):
result_color = partialWhite (flood-opacity=0.9) × in2_A(1.0)
feFlood flood-opacity=0.9 → in_A=0.9, in_R/G/B=1.0
feComposite in: result_A = in_A × in2_A = 0.9 × 1.0 = 0.9
result_R = in_R × in2_A = 1.0 × 1.0 = 1.0
Rendered over white (background): premultiplied alpha blending:
out_R = result_A × result_R + (1-result_A) × bg_R
= 0.9 × 1.0 + 0.1 × 1.0 = 1.0 → white
WCAG: glyph is 90% opacity white → effectively white → 1.00:1
Even at 70% flood-opacity: out_R = 0.7×1.0 + 0.3×1.0 = 1.0 → still white
The attack works at any flood-opacity because white (RGB 1.0) blended
with white background at any opacity = white (1.0).
Detection must not gate on flood-opacity=1.0 exact match. */
SA-CSS-FCI-004: JS mousedown operator over→in — TOCTOU clip-to-shape whiteout
This TOCTOU variant follows the SA-CSS pattern established for atop (SA-CSS-CAT-004), xor (SA-CSS-CXO-004), and out (SA-CSS-COU-004): the feComposite operator is initialized to "over" (the normal pass-through) and swapped to "in" at mousedown. The consent text appears correctly during the reading period and becomes white at confirmation.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="fci004" color-interpolation-filters="sRGB">
<feFlood flood-color="white" flood-opacity="1" result="whiteFill"/>
<!-- Initially operator="over": whiteFill over SourceGraphic.
feComposite over = source over dest = whiteFill on top.
But wait: SourceGraphic has transparent background, so whiteFill
is drawn on top of it. This produces white everywhere — that's wrong.
Let's use operator="xor" as the identity-like state with both in/in2 swapped:
Actually use a simpler approach: start feFlood flood-opacity=0 (transparent) -->
<feFlood id="fci004-flood" flood-color="white" flood-opacity="0" result="hiddenFill"/>
<feComposite id="fci004-comp" in="hiddenFill" in2="SourceGraphic" operator="in"/>
</filter>
</defs>
</svg>
<span style="filter:url(#fci004)">
Grant this MCP server write access to your entire file system.
</span>
<button id="consentBtn" onmousedown="activateFci004()">I Agree</button>
<script>
function activateFci004() {
/* At mousedown: change flood-opacity from 0 (transparent, no effect)
to 1.0 (opaque white) → operator=in now produces white glyphs */
document.getElementById('fci004-flood').setAttribute('flood-opacity', '1');
}
</script>
/* TOCTOU: flood-opacity=0 → filter output = 0×SourceGraphic = transparent (passthrough)
At mousedown: flood-opacity=1 → operator=in: white clipped to glyphs = white glyphs
Consent readable at rest, invisible at click.
MutationObserver: watch flood-opacity on feFlood elements in consent ancestor filters */
const mo = new MutationObserver(muts => {
for (const m of muts) {
if (m.attributeName === 'flood-opacity') {
const newVal = parseFloat(m.target.getAttribute('flood-opacity') || '0');
/* Check if this feFlood feeds a feComposite operator=in */
const result = m.target.getAttribute('result') || '';
const filterEl = m.target.closest('filter');
if (filterEl) {
for (const fc of filterEl.querySelectorAll('feComposite')) {
if (fc.getAttribute('in') === result) {
const op = fc.getAttribute('operator') || 'over';
if (op === 'in' && newVal > 0.5) {
reportFinding('CRITICAL', 'SA-CSS-FCI-004', {
newFloodOpacity: newVal, operator: op,
note: 'feFlood opacity activated into feComposite operator=in at mousedown'
});
}
}
}
}
}
}
});
document.querySelectorAll('filter feFlood').forEach(f =>
mo.observe(f, { attributes: true, attributeOldValue: true,
attributeFilter: ['flood-opacity', 'flood-color'] })
);
Summary of SA-CSS-FCI findings
| ID | Severity | Mechanism | WCAG | Evades |
|---|---|---|---|---|
| SA-CSS-FCI-001 | CRITICAL | feFlood white + feComposite operator=in clip-to-glyph | 1.00:1 | CSSOM color/opacity checks |
| SA-CSS-FCI-002 | CRITICAL | feFlood near-white #f0f0f0 + feComposite operator=in | 1.13:1 | flood-color="white" exact-match rules |
| SA-CSS-FCI-003 | HIGH | feFlood white flood-opacity=0.9 + feComposite operator=in | 1.00:1 | flood-opacity=1.0 exact-match prerequisite rules |
| SA-CSS-FCI-004 | CRITICAL | JS mousedown activates flood-opacity from 0→1 into operator=in | 1.00:1 at click | Static analysis, screenshot at rest |