← Blog

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.

By SkillAudit  |  29 Oct 2026

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;
}
CRITICAL
SA-CSS-CAT-001 — feFlood flood-color="white" + feComposite operator="atop" applied to consent text span. White glyph replacement at 1.00:1 contrast. CSSOM color and opacity unchanged. Applies to any opaque flood color that matches the container background.

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. */
CRITICAL
SA-CSS-CAT-002 — feFlood flood-color="#e5e5e5" + feComposite operator="atop". Gray glyph replacement: WCAG ~1.39:1. Evades white-color-specific detection rules. Detection requires contrast-based flood-color analysis, not color-name matching.

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.

HIGH
SA-CSS-CAT-003 — Two-stage feComposite atop chain. First stage (reversed) appears as identity. Second stage composites white atop the all-opaque first stage output. Result: solid white rectangle. Detection requires compositing all stages symbolically; static single-primitive analysis misses the chain.

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 });
CRITICAL
SA-CSS-CAT-004 — JS mousedown transitions feComposite from operator="over" (identity) to operator="atop" (glyph replacement) at click time. TOCTOU: text readable during review, invisible at confirmation. MutationObserver on operator attribute of all feComposite elements in consent ancestor filters is required.

Summary of SA-CSS-CAT findings

IDSeverityMechanismWCAGEvades
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

Related pages