MCP server SVG feColorMatrix matrix consent security
Four SA-CSS-CMM consent attack patterns using feColorMatrix type="matrix" — the SVG full 4×5 channel transformation matrix that maps [R, G, B, A, 1] to [R', G', B', A'], exploited to zero the alpha channel while preserving RGB, force all RGB channels to white while preserving alpha, scale alpha to 1% to evade exact-zero detection, and swap the identity matrix for an attack matrix at click time.
The feColorMatrix SVG filter primitive with type="matrix" accepts a values attribute containing 20 numbers that define a 4×5 transformation matrix (SVG §15.10). The matrix maps input [R, G, B, A, 1] to output [R', G', B', A'] row by row:
R' = a00×R + a01×G + a02×B + a03×A + a04 G' = a10×R + a11×G + a12×B + a13×A + a14 B' = a20×R + a21×G + a22×B + a23×A + a24 A' = a30×R + a31×G + a32×B + a33×A + a34 values = "a00 a01 a02 a03 a04 a10 a11 a12 a13 a14 a20 a21 a22 a23 a24 a30 a31 a32 a33 a34" Identity: "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
The type=matrix variant is the most powerful feColorMatrix form: it subsumes type=saturate, type=hueRotate, and type=luminanceToAlpha as special-case matrices. An attacker with access to the filter DOM can inject any linear transformation of the RGBA channels — full transparency, forced white, alpha inversion, channel crossfeed, or any combination. Security tools that recognize type=saturate with values="0" as an attack do not automatically recognize the equivalent type=matrix saturation-zeroing matrix.
20 numbers, no structural cues: Unlike type=saturate (1 value) or type=hueRotate (1 value), the type=matrix values string has no structural hint about what the matrix does. A reviewer reading values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0" must mentally evaluate the 4th row (A' = 0×R + 0×G + 0×B + 0×A + 0 = 0) to recognize the alpha-zeroing attack. Detection requires parsing the 20 values, constructing the matrix, and symbolically evaluating its effect on [R,G,B,A,1] for the expected foreground color.
SA-CSS-CMM-001: type=matrix alpha-zero row (full transparency via A'=0)
Setting all 5 coefficients in the A' row to zero — a30=a31=a32=a33=a34=0 — produces A'=0 for every input pixel regardless of the input R, G, B, A values. The entire consent element becomes fully transparent. The RGB rows can be left as identity (or any value) — the output RGB channels are irrelevant once A'=0, since compositing premultiplied-alpha RGBA with A=0 produces no visible contribution.
<filter id="alphaZero">
<feColorMatrix type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 0 0"/>
</filter>
<!-- Row 4 (A'): 0×R + 0×G + 0×B + 0×A + 0 = 0; full transparency -->
<!-- CSS opacity: 1; visibility: visible; display: block — all normal -->
<!-- getComputedStyle().opacity reports "1"; no CSS property modified -->
Detection: evaluate the A' row formula at A=1 (opaque input). If A'(A=1) = a33×1 + a34 = a33 + a34 ≤ 0, the element becomes transparent for any opaque input. The condition is independent of R, G, B inputs. The generalized check: for a given type=matrix, compute max A' over all valid [R,G,B,A]=[0..1,0..1,0..1,1] — this is a33 + a34 (clamped to [0,1]) for any input with A=1. If this maximum ≤ 0.05, flag SA-CSS-CMM-001.
SA-CSS-CMM-002: type=matrix fractional alpha scaling (A'=0.01×A bypasses exact-zero detection)
Setting a33=0.01 and all other A' row coefficients to 0 gives A'=0.01×A. For opaque consent text (A=1): A'=0.01 — 1% alpha. The element renders at 1% opacity, functionally invisible against any background, but the output alpha is 0.01, not 0. Detection that checks A' === 0 or outputAlpha === 0 misses this pattern.
<filter id="alphaFrac">
<feColorMatrix type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 0.01 0"/>
</filter>
<!-- A'=0.01×A; A=1 (opaque text)→A'=0.01 (1% opacity) -->
<!-- Passes A'===0 check; passes opacity<0.001 check -->
<!-- Dark text at 1% alpha on white: effective color ≈ rgb(253,253,253) on white -->
<!-- WCAG contrast ≈ 1.0:1; invisible -->
<!-- correct threshold: a33 + a34 < 0.05 when a33 > 0 → flag -->
Fractional-alpha threshold: The correct detection condition for type=matrix alpha attacks is: compute maxOutputAlpha = max(a30, 0)×1 + max(a31, 0)×1 + max(a32, 0)×1 + max(a33, 0)×1 + a34 evaluated at R=G=B=A=1 (all channels maximum). If maxOutputAlpha < 0.05, the element is effectively invisible for any opaque input. This covers both exact-zero (SA-CSS-CMM-001) and near-zero fractional scaling (SA-CSS-CMM-002).
SA-CSS-CMM-003: type=matrix RGB forced-to-white (R'=G'=B'=1 via offset coefficients)
Setting all channel-mixing coefficients (a00–a03, a10–a13, a20–a23) to 0 and all offset coefficients (a04, a14, a24) to 1 forces R'=G'=B'=1 for every pixel regardless of input. With the A' row as identity (a33=1, all others 0), the output is white at the original alpha. For opaque dark consent text: A'=1, R'=G'=B'=1 → white at full opacity. Against a white background, white text on white is invisible.
<filter id="rgbWhite">
<feColorMatrix type="matrix"
values="0 0 0 0 1
0 0 0 0 1
0 0 0 0 1
0 0 0 1 0"/>
</filter>
<!-- R'=0+0+0+0+1=1; G'=1; B'=1 for every input pixel -->
<!-- A' = identity (a33=1): original alpha preserved -->
<!-- All opaque pixels → white, alpha preserved → white at 100% opacity -->
<!-- getComputedStyle().color reports original dark color; detection blind spot -->
The alpha-preservation in SA-CSS-CMM-003 is the critical distinguisher from SA-CSS-CMM-001. In CMM-001, the alpha is zeroed and the element disappears entirely from the compositing stack. In CMM-003, the element has full opacity — it is a white rectangle with the same bounding box and stacking position as the original. The opacity=1 and visibility=visible checks pass, and a contrast measurement tool reading getComputedStyle().color computes the original dark contrast. The attack is an RGB-channel override, not a transparency attack.
Detection requires evaluating the rendered output color, not just the alpha. For type=matrix, compute the R', G', B' values for the expected foreground color of the consent text. If R'≥0.95 AND G'≥0.95 AND B'≥0.95 for the text foreground pixels, the output color is near-white regardless of the original text color. Compare this output color against the background to determine WCAG contrast failure.
SA-CSS-CMM-004: JS mousedown replaces identity matrix with white-out matrix (click-time activation)
The initial filter uses the identity matrix: values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0". The consent element renders correctly. On mousedown, the script replaces the values attribute with the SA-CSS-CMM-003 white-out matrix: all RGB offset = 1, alpha identity. The switch is a single setAttribute call.
consentElement.addEventListener("mousedown", () => {
const matrix = document.getElementById("colorMatrix")
.querySelector("feColorMatrix");
// Replace identity with RGB-white-out matrix
matrix.setAttribute("values",
"0 0 0 0 1 " + // R'=1 (white)
"0 0 0 0 1 " + // G'=1 (white)
"0 0 0 0 1 " + // B'=1 (white)
"0 0 0 1 0" // A'=A (preserve alpha)
);
// Consent text → white on white; invisible at instant of install-click
// getComputedStyle().color throughout: original dark color; no CSS change
});
The identity matrix initial state is the strongest possible static indicator of legitimacy — it is the mathematical no-op. Any static analysis tool evaluating the feColorMatrix at load time sees an identity transformation and concludes the filter has no effect. The attack is entirely dynamic. MutationObserver on the values attribute of feColorMatrix elements with type=matrix within consent ancestor filters is required. The observer callback must re-evaluate the full matrix against the expected foreground color on each attribute change, not just check for simple patterns like all-zeros or all-ones.
Attack summary
| ID | Matrix effect | Attack mechanism | Evasion | Severity |
|---|---|---|---|---|
| SA-CSS-CMM-001 | A' row = all zeros | Full transparency | opacity:1 unchanged | Critical |
| SA-CSS-CMM-002 | a33=0.01 (A scaled) | 1% alpha; near-invisible | Passes A'===0 check | Critical |
| SA-CSS-CMM-003 | RGB offset=1 (forced white) | White text on white bg | Alpha preserved; getComputedStyle blind | Critical |
| SA-CSS-CMM-004 | identity→white-out mousedown | Identity at load; attack at click | Static analysis sees no-op | Critical |
Defences
- Parse and symbolically evaluate the full 4×5 matrix: For any feColorMatrix type=matrix in a consent ancestor filter, parse all 20 values. Evaluate the A' formula at A=1 to determine maximum output alpha. Evaluate the R', G', B' formulas at the expected foreground color to determine output color. Compute WCAG contrast of the output color against the background at the output alpha. Flag if contrast < 4.5:1 (AA) or output alpha < 0.05.
- Use a threshold, not exact-zero, for alpha detection: Flag when maxOutputAlpha(A=1) < 0.05 — not < 0 or === 0. The fractional-scaling attack (SA-CSS-CMM-002) exploits exact-zero checks. The correct threshold is a rendered-opacity floor.
- Detect RGB-white-out independently from alpha attacks: Check both the output alpha AND the output RGB color. An attack that forces R'=G'=B'=1 at full alpha is as effective as an alpha-zero attack on a white background, but requires different detection logic (output RGB comparison vs alpha threshold).
- MutationObserver on feColorMatrix values attribute: Re-evaluate the full matrix on every attribute change. The identity-to-attack pattern (SA-CSS-CMM-004) requires a dynamic check; static analysis of the initial values is insufficient. The observer callback must evaluate the post-change matrix, not just flag the change event.
SkillAudit findings for this attack surface
feColorMatrix type="matrix" with A' row all-zero (a30=a31=a32=a33=a34=0) — A'=0 for all pixels; full transparency; CSS opacity:1, visibility:visible unchanged; getComputedStyle().opacity reports "1"; detection: evaluate A' formula at A=1; flag if a33+a34 < 0.05.feColorMatrix type="matrix" with a33=0.01 (fractional alpha scaling) — A'=0.01×A; opaque text→1% opacity; functionally invisible; passes A'===0 and alpha<0.001 exact checks; correct threshold: maxOutputAlpha(A=1)=a33+a34 where a33=0.01; 0.01<0.05 → flag.feColorMatrix type="matrix" with RGB offset all-1 (a04=a14=a24=1, cross-coefficients=0, A'=identity) — R'=G'=B'=1; A'=original; white pixels at full alpha; white text on white background; getComputedStyle().color unchanged; detection: evaluate R'G'B' at expected foreground color; flag if output near-white (R',G',B'≥0.95) AND background near-white.