← Blog

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.

By SkillAudit  |  24 Oct 2026

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

IDMatrix typeGreen badge WCAGYellow badge WCAGDetection evasionSeverity
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

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CMG-001: 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.
CRITICAL SA-CSS-CMG-002: 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.
CRITICAL SA-CSS-CMG-004: JS mousedown replaces identity with BT.709 grayscale matrix — static analysis: identity (benign); click-time: all chromatic badge colors collapse to gray; WCAG failures for green/yellow badges; MutationObserver on feColorMatrix values; re-run structural grayscale check on every attribute change.

Related: feColorMatrix type=saturate values=0 (SA-CSS-SAT)  |  feColorMatrix sepia badge collapse (SA-CSS-CME)  |  feColorMatrix near-white bias (SA-CSS-CMB)

← Blog  |  Security Checklist