← Blog

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.

By SkillAudit  |  29 Oct 2026

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;
}
CRITICAL
SA-CSS-FCI-001 — feFlood white + feComposite operator=in. White fill clipped to glyph alpha map. White glyphs on white background: 1.00:1. CSSOM color/opacity unchanged. Detection: feComposite operator=in with feFlood as in source → CRITICAL.

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 */
CRITICAL
SA-CSS-FCI-002 — feFlood near-white #f0f0f0 + feComposite operator=in. WCAG ≈1.13:1. Evades flood-color="white" specific rules. Detection: compute flood-color luminance; L≥0.6 over white container → 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. */
HIGH
SA-CSS-FCI-003 — feFlood white flood-opacity=0.9 + feComposite operator=in. White at any opacity over white background = white (1.00:1). Evades flood-opacity=1.0 exact-match prerequisite rules. Detection must not require flood-opacity=1.

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'] })
);
CRITICAL
SA-CSS-FCI-004 — feFlood starts flood-opacity=0 (invisible); swapped to 1 at mousedown. feComposite operator=in activates white glyph fill. TOCTOU: consent readable at rest, white at click. MutationObserver on feFlood flood-opacity feeding feComposite operator=in required.

Summary of SA-CSS-FCI findings

IDSeverityMechanismWCAGEvades
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

Related pages