MCP server SVG feColorMatrix grayscale consent security
Four SA-CSS-CMG consent attack patterns using feColorMatrix type="matrix" with grayscale weights — the BT.709 variant produces results numerically identical to feColorMatrix type="saturate" values="0" while evading any detection rule written to check only the type attribute, and where non-standard 1/3-average coefficients additionally evade BT.709-specific checks, all while collapsing green and yellow permission badges to failing-WCAG gray values on white backgrounds.
A grayscale conversion transforms any color input to a single luminance value, setting R’=G’=B’=luminance. The W3C-standard BT.709 grayscale formula uses weights [0.2126, 0.7152, 0.0722] matching the sRGB primaries. These exact same weights appear in feColorMatrix type="saturate" values="0" — the CSS filter:grayscale(1) equivalent. However, an attacker can implement the same grayscale transformation using feColorMatrix type="matrix" with explicit BT.709 coefficients in all three RGB output rows. The two implementations are mathematically identical but use entirely different SVG attribute strings. A detection rule that flags type="saturate" with low values does not match type="matrix" with BT.709 coefficients.
Detection-path gap: feColorMatrix type="saturate" values="0" and feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 ..." produce identical pixel output but require separate detection rules. A scanner that only checks for type="saturate" with low values completely misses the type=matrix equivalent. Both must be enumerated. The non-standard 1/3-average variant adds a third code path that neither rule covers.
Grayscale matrix variants
/* BT.709 grayscale (same as type=saturate values=0): */
values="0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"
/* R' = G' = B' = 0.2126×R + 0.7152×G + 0.0722×B = luminance */
/* NTSC/BT.601 grayscale (older standard): */
values="0.299 0.587 0.114 0 0
0.299 0.587 0.114 0 0
0.299 0.587 0.114 0 0
0 0 0 1 0"
/* Different weights; similar result for common colors; separate detection path */
/* Simple 1/3 average grayscale (non-perceptual): */
values="0.333 0.333 0.333 0 0
0.333 0.333 0.333 0 0
0.333 0.333 0.333 0 0
0 0 0 1 0"
/* Evades both BT.709 and BT.601 coefficient checks; badge-color collapse still occurs */
/* All three share the structural signature:
- All three RGB output rows have EQUAL values (same coefficients)
- Sum of first three columns in each row = approximately 1.0
- Fourth column (alpha coefficient) = 0
This structural signature is the detection-robust characteristic. */
SA-CSS-CMG-001: BT.709 grayscale via type=matrix — evades type=saturate detection
The BT.709 grayscale matrix sets R’=G’=B’=L for every pixel, where L=0.2126R+0.7152G+0.0722B is the relative luminance. All color distinctions are destroyed. Permission badges with different hues but similar luminance become the same shade of gray — visually indistinguishable.
<filter id="cmg001">
<feColorMatrix type="matrix"
values="0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"/>
</filter>
<!-- Numerically identical to: <feColorMatrix type="saturate" values="0"/>
Detection rule "type === 'saturate'" does not match this.
Detection must additionally check type=matrix for grayscale weight pattern. -->
/* Green safe-badge #22C55E → gray badge analysis:
Linear: R=0.016, G=0.557, B=0.116
L = 0.2126×0.016 + 0.7152×0.557 + 0.0722×0.116
= 0.003 + 0.398 + 0.008 = 0.409
Output: RGB(0.409, 0.409, 0.409) — medium gray
WCAG vs white: (1+0.05)/(0.409+0.05) = 1.05/0.459 = 2.29:1 → FAIL AA (need 4.5:1)
Red danger-badge #EF4444 → gray badge analysis:
Linear: R=0.871, G=0.057, B=0.057
L = 0.2126×0.871 + 0.7152×0.057 + 0.0722×0.057 = 0.185+0.041+0.004 = 0.230
Output gray: L=0.230
WCAG vs white: (1.05)/(0.280) = 3.75:1 → FAIL AA
Yellow warning-badge #FBBF24 → gray badge analysis:
Linear: R=0.961, G=0.527, B=0.017
L = 0.2126×0.961 + 0.7152×0.527 + 0.0722×0.017 = 0.204+0.377+0.001 = 0.582
Output gray: L=0.582
WCAG vs white: (1.05)/(0.632) = 1.66:1 → FAIL AA — near-invisible
BLACK text #000000: L=0 → output gray=0 (still black) → TRUE NEGATIVE
DARK text #333333: L≈0.033 → output gray=0.033 → WCAG 21:1 → still readable */
Three of the four most common permission-badge colors fail WCAG AA against white after BT.709 grayscale. Only pure black text passes. A consent dialog with colored permission badges becomes a collection of failing-contrast gray squares where no badge is readable at the 4.5:1 minimum. Black body text remains readable, but the color-severity signal is completely destroyed.
SA-CSS-CMG-002: non-standard 1/3-average grayscale — evades BT.709-specific detection
The simple average grayscale formula L’=(R+G+B)/3 uses equal channel weights. Its results differ from BT.709 because BT.709 is perceptually weighted (green contributes 71.5%, red 21.3%, blue 7.2%); the 1/3 average over-weights blue and under-weights green. For a green safe badge this means the output luminance is lower than BT.709 (blue is under-estimated by BT.709 but not by 1/3 average for this specific input). Detection rules that check for exact BT.709 values (0.2126, 0.7152, 0.0722) will not match the 0.333 variant.
<filter id="cmg002">
<feColorMatrix type="matrix"
values="0.333 0.333 0.333 0 0
0.333 0.333 0.333 0 0
0.333 0.333 0.333 0 0
0 0 0 1 0"/>
</filter>
/* Green badge #22C55E with 1/3 average:
L' = (0.016 + 0.557 + 0.116) / 3 = 0.689 / 3 = 0.230
Output gray: 0.230
WCAG: 1.05/0.280 = 3.75:1 → FAIL (different from BT.709 0.409 but still fails)
Detection: check structural signature, not exact coefficients:
1. All three RGB output rows have the same three column values
2. The sum of those three values ≈ 1.0
3. No single coefficient exceeds 0.80 (which would indicate non-grayscale mixing)
If all three conditions: flag as CRITICAL on consent elements. */
SA-CSS-CMG-003: partial grayscale — blend between identity and full grayscale
A partial grayscale matrix linearly interpolates between the identity matrix and the full grayscale matrix: at blend factor t=0 it is the identity, at t=1 it is full grayscale. At t=0.5, each RGB output row is half-identity + half-grayscale. This produces a desaturated but not fully monochromatic output — colors are muted but not completely eliminated. Even at t=0.5, colored permission badges are substantially desaturated and the severity signal is significantly degraded.
/* Partial grayscale at blend=0.5: interpolate(identity, BT.709, 0.5)
Identity row for R: [1, 0, 0, 0, 0]
BT.709 row (same for all): [0.2126, 0.7152, 0.0722, 0, 0]
Blended R row at t=0.5:
[0.5×1+0.5×0.2126, 0.5×0+0.5×0.7152, 0.5×0+0.5×0.0722, 0, 0]
= [0.6063, 0.3576, 0.0361, 0, 0]
Similarly for G and B rows (each row blends toward BT.709 all-same):
G row: [0.1063, 0.8576, 0.0361, 0, 0]
B row: [0.1063, 0.3576, 0.5361, 0, 0]
Green badge after 50% partial grayscale:
Linear R=0.016, G=0.557, B=0.116
R' = 0.6063×0.016 + 0.3576×0.557 + 0.0361×0.116 = 0.010+0.199+0.004 = 0.213
G' = 0.1063×0.016 + 0.8576×0.557 + 0.0361×0.116 = 0.002+0.478+0.004 = 0.484
B' = 0.1063×0.016 + 0.3576×0.557 + 0.5361×0.116 = 0.002+0.199+0.062 = 0.263
→ Desaturated teal/green: still partially green but much more muted
Output luminance: 0.2126×0.213+0.7152×0.484+0.0722×0.263 = 0.045+0.346+0.019=0.410
WCAG vs white: 1.05/0.460 = 2.28:1 → FAIL AA
Red badge → desaturated brownish at t=0.5 (less red, more neutral) → WCAG ~3.5:1 → FAIL
→ Even 50% partial grayscale causes WCAG AA failures on common badge colors */
SA-CSS-CMG-004: JS mousedown identity → grayscale at click time
The initial filter is the identity matrix; consent renders with correct badge colors. On mousedown, the script replaces values with the BT.709 grayscale matrix. All chromatic elements collapse to gray at the moment of confirmation.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="cmg004">
<feColorMatrix id="cmg004M" 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(#cmg004)">
<span class="badge red">CRITICAL: shell execution</span>
<span class="badge green">SAFE: config read</span>
<button id="confirm">Allow</button>
</div>
<script>
document.getElementById("confirm").addEventListener("mousedown", () => {
document.getElementById("cmg004M").setAttribute("values",
"0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");
});
</script>
Attack summary
| ID | Matrix type | Green badge WCAG | Yellow badge WCAG | Detection evasion | Severity |
|---|---|---|---|---|---|
| SA-CSS-CMG-001 | BT.709 via type=matrix | 2.29:1 (fail) | 1.66:1 (fail) | type=saturate check misses this | Critical |
| SA-CSS-CMG-002 | 1/3 average via type=matrix | 3.75:1 (fail) | 1.66:1 (fail) | BT.709 coefficient check misses this; BT.601 check also misses | Critical |
| SA-CSS-CMG-003 | 50% partial grayscale blend | 2.28:1 (fail) | ≈2.5:1 (fail) | Not identical to any standard formula; structural check required | High |
| SA-CSS-CMG-004 | BT.709 (mousedown) | Identity at load; gray at click | Identity at load; gray at click | Static analysis sees identity; click-time attack | Critical |
Defences
- Detect grayscale via structural matrix signature, not exact coefficients: The grayscale structural signature is: all three RGB output rows (values[0–2], values[5–7], values[10–12]) contain the same three values, and those three values sum to approximately 1.0. This covers BT.709, BT.601, 1/3 average, and any other luminance-weighting scheme. Do not rely on exact coefficient matching (0.2126 === coefficient) because any set of positive weights summing to 1.0 that are equal across rows implements grayscale.
- Enumerate all grayscale code paths: Flag (1)
type="saturate"with values below 0.05, (2)type="matrix"with the structural grayscale signature, and (3)type="matrix"with any three RGB rows where all row values are identical (regardless of whether they sum to 1.0 — equal-row patterns with sum<1.0 implement dimming in addition to grayscale). All three code paths require the same consent-panel flag. - True-negative for achromatic consent panels: If the consent panel uses only black text on white background (no chromatic badge colors), grayscale has no effect. Lower the severity to LOW if no chromatic elements are detected in the consent panel. If chromatic elements are present (colored badges, colored scope labels), maintain CRITICAL.
- MutationObserver and structural check on every values change: Re-run the structural signature test on every feColorMatrix values attribute change. The identity→grayscale transition (SA-CSS-CMG-004) fires at mousedown. Block confirmation if detected.
SkillAudit findings for this attack surface
feColorMatrix type="matrix" with BT.709 grayscale weights (0.2126/0.7152/0.0722 equal across all three RGB rows) on consent panel — numerically identical to type=saturate values=0 but different code path; green badge WCAG 2.29:1 on white (fail AA); yellow badge WCAG 1.66:1 (fail); black text unchanged (true negative); detection requires structural matrix check, not type=saturate string match.feColorMatrix type="matrix" with 1/3-average grayscale weights (0.333 equal across all three RGB rows) on consent panel — different from BT.709 numerically; evades both type=saturate and BT.709-specific checks; green badge WCAG 3.75:1 (fail); yellow badge WCAG 1.66:1 (fail); detect via structural signature: equal RGB rows summing to ~1.0.