MCP server CSS feColorMatrix security: SVG feColorMatrix filter type matrix coefficient attack, saturate desaturation to gray, hueRotate background-matching, and CSS filter attribute override on consent text
Published 2026-09-26 — SkillAudit Research
The SVG feColorMatrix filter primitive transforms the color and alpha values of each input pixel using one of four computation modes: type="matrix" (full 5×4 RGBA matrix multiplication), type="saturate" (scalar saturation adjustment), type="hueRotate" (hue rotation in degrees), and type="luminanceToAlpha" (replaces alpha channel with luminance). Each mode produces a pixel-level color transformation that is completely independent of the element's fill attribute. Standard contrast checkers that inspect getComputedStyle(el).fill or the SVG fill attribute will see the original fill color — not the filter output color — and will produce an incorrect contrast reading.
An MCP server can exploit this gap by using feColorMatrix to transform the rendered appearance of consent text while preserving its fill attribute. The matrix type is especially powerful: its 20 free coefficients can map any input color to any output color, including mapping dark consent text to near-white. The saturate and hueRotate types are more constrained but can still degrade contrast in targeted ways, particularly when the background color is specifically chosen to exploit the luminance or hue family of the desaturated or rotated output. CSS can additionally override the values presentation attribute, so the SVG source may show a benign or identity matrix while the browser applies an attack matrix via the CSS cascade.
Key evasion property: In all feColorMatrix attacks, fill, getComputedStyle(el).fill, opacity, visibility, display, and getBoundingClientRect checks pass normally. The consent text is present in the DOM with correct fill colors and positive dimensions. Only simulation of the feColorMatrix output — by applying the matrix to the element's fill color and computing contrast of the result — reveals the attack.
Attack findings
A feColorMatrix of
type="matrix" is applied to consent text. The 5×4 RGBA matrix is carefully constructed so that dark consent text (#1a1a1a) maps to a near-white output (#e8e8e8). The matrix values look like a subtle tint/desaturate effect to a visual reviewer. The consent text is present in the DOM with correct fill colors; the matrix transform is the attack vector. Standard contrast checkers that inspect element fill color rather than filter output color miss this attack.
<filter id="consent-tint">
<!-- Maps #1a1a1a (dark text) to #e8e8e8 (near-white) -->
<!-- Looks like a subtle tint; is actually a near-full-erase -->
<feColorMatrix type="matrix" values="
0.1 0 0 0 0.89
0 0.1 0 0 0.89
0 0 0.1 0 0.89
0 0 0 1 0
"/>
</filter>
<!--
Input: rgb(26, 26, 26) → dark consent text
R_out = 0.1*(26/255) + 0.89 ≈ 0.9002 → ~229/255 → #e5e5e5
Contrast vs #ffffff: ~1.28:1 → far below 3:1 minimum
fill="#1a1a1a" check PASSES (fill is dark)
getComputedStyle(el).fill → "rgb(26, 26, 26)" PASSES
Only filter output simulation reveals attack -->
Detection: simulate feColorMatrix output by applying the matrix multiplication to the element's fill color; compute contrast of simulated output against background.
<feColorMatrix type="saturate" values="0"/> desaturates the consent text to its luminance equivalent. For dark navy text (#1a2a4a), desaturation produces a mid-gray (~#3a3a3a, contrast 6:1 vs white — still readable). But if the consent background is a light warm gray (#f0ede8) and text is a mid-blue (#5566aa), desaturation maps #5566aa to its luminance gray (~#888) which has contrast ~2.5:1 against #f0ede8. The attack exploits the combination of a specific text color choice and a light background that reduces contrast specifically for that text's luminance value.
<filter id="consent-desat">
<feColorMatrix type="saturate" values="0"/>
</filter>
<!-- Consent text: fill="#5566aa" (mid-blue) on background #f0ede8 (warm gray)
Original contrast: #5566aa vs #f0ede8 = 3.8:1 (passes)
Desaturated: luminance(#5566aa) ≈ 0.30 → gray(~#888) vs #f0ede8 → 2.3:1 (fails)
type="saturate" values="0" looks like a grayscale/print style, not an attack -->
<feColorMatrix type="hueRotate" values="210"/> rotates the hue of consent text. For a warm background (#f5f0e8) and blue-gray text (#2d4a6b), a 210° hue rotation shifts the text color toward warm amber — near the background hue family. The rotated text on a warm background has reduced contrast compared to the original cool-on-warm pairing. The specific rotation value is chosen to maximize color-matching between rotated text and background hue family.
<filter id="consent-hue">
<!-- 210° hue rotation: shifts blue-gray text toward background amber family -->
<feColorMatrix type="hueRotate" values="210"/>
</filter>
<!-- Original: #2d4a6b (cool blue-gray) vs #f5f0e8 (warm cream) → high contrast
Rotated: hue rotated 210° → warm-ish mid-color → closer to background family
Specific contrast depends on rotation value and original text/background pair;
attacker picks rotation to minimize contrast for target text/background combo -->
The feColorMatrix SVG element has
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" (identity matrix — pass-through) as a visible attribute. A CSS rule in a <style> element or external stylesheet overrides this. Because values is an SVG presentation attribute, the CSS cascade can override it with higher specificity. An auditor reading the SVG source sees the identity matrix; the browser applies the override matrix.
<!-- SVG attribute: identity matrix (benign, pass-through) -->
<feColorMatrix id="consent-matrix" type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
<!-- CSS (in <style> or external) overrides values attribute via presentation attribute cascade -->
<style>
#consent-matrix {
/* Overrides SVG attribute: attack matrix mapped dark to near-white */
values: 0.1 0 0 0 0.89 0 0.1 0 0 0.89 0 0 0.1 0 0.89 0 0 0 1 0;
}
</style>
<!-- getAttribute('values') → "1 0 0 0 0 0 1 0 0 0 ..." → PASS (identity)
Browser applies CSS values → attack matrix applied to consent text -->
Detection
function checkFeColorMatrix(svgRoot) {
const findings = [];
const filters = svgRoot.querySelectorAll('filter');
for (const filter of filters) {
const matrices = filter.querySelectorAll('feColorMatrix');
for (const matrix of matrices) {
// Check for CSS override of values attribute
const attrValues = matrix.getAttribute('values');
const cssValues = getComputedStyle(matrix).values; // may not be exposed in all browsers
const type = matrix.getAttribute('type') || 'matrix';
if (type === 'saturate') {
const satVal = parseFloat(matrix.getAttribute('values') || '1');
if (satVal < 0.2) {
findings.push({ severity: 'high', issue: `feColorMatrix saturate=${satVal} — check desaturated contrast for low-luminance text on warm backgrounds` });
}
}
if (type === 'matrix' && attrValues) {
// Simulate matrix on consent text fill color
// Parse 20 values, apply 5x4 matrix to RGBA input
const vals = attrValues.trim().split(/\s+/).map(Number);
if (vals.length === 20) {
// Apply to dark text: input rgb(26,26,26,255) normalized
const r=26/255, g=26/255, b=26/255, a=1;
const r_out = vals[0]*r + vals[1]*g + vals[2]*b + vals[3]*a + vals[4];
const g_out = vals[5]*r + vals[6]*g + vals[7]*b + vals[8]*a + vals[9];
const b_out = vals[10]*r + vals[11]*g + vals[12]*b + vals[13]*a + vals[14];
// If output is near-white (r_out, g_out, b_out all > 0.8), flag
if (r_out > 0.75 && g_out > 0.75 && b_out > 0.75) {
findings.push({ severity: 'critical', issue: `feColorMatrix matrix maps dark text to near-white: output rgb(${Math.round(r_out*255)},${Math.round(g_out*255)},${Math.round(b_out*255)}) — contrast likely below 3:1` });
}
}
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
| Simulate feColorMatrix output by applying its matrix to the consent element's fill color, then compute contrast of simulated output vs background | The fill attribute is not the rendered color when a feColorMatrix filter is present; only the simulated output color reflects what the user sees |
Check both SVG attribute and CSS computed values for feColorMatrix values — CSS can override presentation attributes | CSS can silently change the matrix used by the browser while the SVG attribute shows a benign or identity matrix; attribute-only auditing misses this override |
For type="saturate", compute the luminance-equivalent gray of the text color and check its contrast against the consent background | Desaturation maps text to its luminance gray; contrast against a warm or mid-tone background depends on the specific luminance value and may fail even when the original colored text passes |
| Flag any feColorMatrix on consent text elements and require explicit audit of its output contrast, not the input fill color | All feColorMatrix types — matrix, saturate, hueRotate, luminanceToAlpha — can degrade contrast in ways that bypass fill-color-based contrast checkers; output simulation is the only reliable detection |
SkillAudit simulates feColorMatrix output for each filter type by applying the matrix, saturation, or hue rotation to the consent element's fill color, then computes contrast of the simulated output against the effective background. CSS overrides to presentation attributes are detected via getComputedStyle. Run a free audit on any MCP server GitHub URL to detect feColorMatrix consent text attacks across the full SVG filter attack surface.