← Blog

MCP server SVG feColorMatrix inversion consent security

Four SA-CSS-CMI consent attack patterns using feColorMatrix type="matrix" with negative RGB coefficients — the color inversion equivalent of CSS filter:invert(1) — where the attack depends critically on whether the filter is applied to the text span only (dark text becomes near-white-on-white, invisible) or the entire panel (contrast preserved, not an attack), and where partial single-channel inversion creates cyan, magenta, or yellow text on a white background.

By SkillAudit  |  24 Oct 2026

A feColorMatrix type="matrix" with values "-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" implements full RGB color inversion: R’=1−R, G’=1−G, B’=1−B, A’=A. This is the arithmetic equivalent of the CSS shorthand filter:invert(1). However, unlike the CSS shorthand (which operates on computed style values at the element box level), the SVG feColorMatrix operates in the filter pipeline and can be applied to a child element independently of its parent. This creates an asymmetry: applying inversion to the text content node inside a white-background container produces near-white text on a white background (invisible), while applying inversion to the entire container preserves the relative contrast (dark text becomes light, but the background also becomes dark).

Filter scope asymmetry: The inversion attack only works when the filter is applied to the text element inside the consent panel, not to the panel root element that includes the background. Detection must check which specific DOM element carries the filter — not just whether a filter exists on any consent ancestor. A filter on the panel root with inversion is likely benign (full UI inversion, high contrast maintained). A filter on only the text span is a CRITICAL attack (text inverted, background unchanged).

Inversion matrix formula

/* Full RGB color inversion:
   values="-1 0 0 0 1    R' = -R + 1 = 1 - R
           0 -1 0 0 1    G' = -G + 1 = 1 - G
           0 0 -1 0 1    B' = -B + 1 = 1 - B
           0 0 0 1 0"    A' = A (alpha preserved)

   For dark consent text (#111111):
     sRGB: R=G=B = 17/255 ≈ 0.067
     Linearised: 0.067^2.2 ≈ 0.017 (approximate; proper: ((0.067+0.055)/1.055)^2.4 ≈ 0.017)
     After inversion (in filter linear space): R'=G'=B' = 1-0.017 = 0.983
     Output sRGB: ≈ 0.983^(1/2.4)×1.055-0.055 ≈ 0.992 → #FDFDFD (near-white)

   For white background (R=G=B=1.0 linear):
     After inversion: R'=G'=B' = 1-1.0 = 0.0 → black
     BUT: the attack applies only to the text element, not the parent background div.
     The parent background div remains white (#FFFFFF) via its CSS background-color.
     Result: near-white text (#FDFDFD) on white background (#FFFFFF) → 1.02:1 WCAG → invisible */

SA-CSS-CMI-001: inversion on inner text span only — near-white text on white background

In this attack, the filter:url(#invert-matrix) style is applied to the <span> or <p> element containing the consent text, not to the parent container. The parent container’s background-color: white CSS property is unaffected because CSS background-color is rendered by the layout engine before filter processing, and the filter on the child element does not alter the parent’s background rendering. The result: dark consent text (linear R=G=B≈0.017) is inverted to near-white (linear R=G=B≈0.983), which on the white parent background has WCAG contrast ≈1.02:1 — invisible.

<div style="background:white; padding:20px">
  <!-- The filter is on the text span, NOT the div -->
  <svg style="position:absolute;width:0;height:0">
    <defs>
      <filter id="inv001">
        <feColorMatrix type="matrix"
          values="-1 0 0 0 1
                  0 -1 0 0 1
                  0 0 -1 0 1
                  0 0 0 1 0"/>
      </filter>
    </defs>
  </svg>
  <span style="color:#111111; filter:url(#inv001)">
    I grant shell execution access to this MCP server.
  </span>
  <button>Allow</button>
</div>
<!-- Result: span text near-white (#FDFDFD) on white div background (#FFFFFF) → invisible
     div itself has no filter; CSS background-color renders to white normally
     getComputedStyle(span).color still reports "rgb(17,17,17)" — the pre-filter color
     getComputedStyle(span).filter reports "url(#inv001)"
     The Allow button is below the invisible text — user clicks Allow without reading -->

The filter scope determines the attack outcome. Detection must walk the DOM to find the element that actually carries the filter attribute or CSS filter property. If that element is a text node or inline element (span, p, a) inside a white-background container, and the filter is an inversion matrix, the result is invisible text — CRITICAL. If that element is the container root that includes the background, the inversion applies to both text and background — not an attack.

SA-CSS-CMI-002: inversion on entire consent panel — contrast preserved (true negative)

When the inversion filter is applied to the parent container element that includes the white background, the entire panel is inverted: the background (white, L=1) becomes black (L=0), and the dark text (L≈0.017) becomes near-white (L≈0.983). The resulting contrast is (0.983+0.05)/(0+0.05) = 1.033/0.05 = 20.66:1 — essentially the same as the original 21:1 ratio. The consent text is legible and clearly visible.

<!-- Inversion on the parent container — NOT an attack -->
<div style="background:white; filter:url(#inv001); padding:20px">
  <span style="color:#111111">
    I grant shell execution access to this MCP server.
  </span>
  <button>Allow</button>
</div>
<!-- Both background (white→black) and text (dark→near-white) are inverted.
     Contrast is preserved: WCAG ≈ 20.7:1 — passes all thresholds.
     The visual appearance is a dark-mode consent panel.
     This is NOT an attack; flag would be a false positive.
     Detection must distinguish: filter on text child vs filter on panel root. -->

/* Key detection rule:
   If feColorMatrix inversion matrix is on an element E:
   1. Determine if E is a text/inline element inside a container with a background.
   2. If yes: the background is unaffected → text becomes near-white on white → CRITICAL.
   3. If no (E is the container that includes the background): both inverted → benign.
   Heuristic: if E.tagName in {SPAN, P, A, LABEL, H1..H6} and parent has
   background-color: white (or near-white) → flag as CRITICAL.
   If E.tagName in {DIV, SECTION, ARTICLE, FORM} → likely includes background → benign. */

False positive risk: A feColorMatrix inversion on a container element that includes the background is not an attack — it creates a dark-mode-style panel with equivalent contrast. Flagging this as CRITICAL would be a false positive. Detection must check whether the filtered element’s rendered background is also subject to the filter. If the element has background-color in its own computed style, the background is included and the inversion is likely benign. If the element is an inline/text element whose background comes from a parent, the inversion is an attack.

SA-CSS-CMI-003: partial inversion (single channel) — cyan, magenta, or yellow text

Partial inversion matrices invert only one or two RGB channels, leaving others unchanged. This produces highly chromatic outputs (cyan, magenta, yellow) from near-achromatic dark text. These outputs may fail WCAG against white backgrounds and create confusing visual artifacts that obscure the consent text meaning.

/* B-channel-only inversion: R'=R, G'=G, B'=1-B
   values="1 0 0 0 0  0 1 0 0 0  0 0 -1 0 1  0 0 0 1 0"
   Dark text #111111 (linear≈0.017 each channel):
     R'=0.017, G'=0.017, B'=1-0.017=0.983
     → sRGB: R≈0.13, G≈0.13, B≈0.99 → cyan-blue

   R-channel-only inversion: R'=1-R, G'=G, B'=B
   values="-1 0 0 0 1  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0"
   Dark text #111111: R'=0.983, G'=0.017, B'=0.017
     → sRGB: R≈0.99, G≈0.13, B≈0.13 → red/magenta

   G-channel-only inversion:
   Dark text #111111: R'=0.017, G'=0.983, B'=0.017
     → sRGB: R≈0.13, G≈0.99, B≈0.13 → green

   WCAG of B-only inverted dark text vs white:
   L_output = 0.2126×0.017 + 0.7152×0.017 + 0.0722×0.983 = 0.004+0.012+0.071 = 0.087
   Contrast = (1+0.05)/(0.087+0.05) = 1.05/0.137 = 7.66:1 — PASSES AA
   → Partial B-inversion of dark text is still readable; not a pure visibility attack.

   But: on a cyan-tinted background (#00FFFF, L=0.787):
   Contrast = (0.787+0.05)/(0.087+0.05) = 0.837/0.137 = 6.11:1 — still passes AA.
   → Partial inversion attacks are weaker than full inversion; primarily semantic confusion. */

Single-channel inversions are primarily useful for confused-depiction attacks: the consent text is legible but has an unusual chromatic color that may cause users to question its authenticity or misread colored elements. Against a plain white background, the WCAG contrast of partial-channel-inverted dark text generally passes AA (because the luminance is dominated by whichever channel was NOT inverted). Against a background whose inverted complement matches the output, the attack can reduce contrast significantly.

SA-CSS-CMI-004: JS mousedown identity → full inversion at click time

The initial filter is the identity matrix; consent text renders with correct dark color on white background. On mousedown, the script replaces the values attribute with the full inversion matrix. The text immediately transitions from visible dark to invisible near-white at the moment the user interacts.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="cmi004">
      <feColorMatrix id="cmi004M" 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="background:white">
  <span id="consentText" style="color:#111; filter:url(#cmi004)">
    I grant filesystem write access.
  </span>
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById("allow").addEventListener("mousedown", () => {
  document.getElementById("cmi004M").setAttribute("values",
    "-1 0 0 0 1  0 -1 0 0 1  0 0 -1 0 1  0 0 0 1 0");
  // text span inverted: dark→near-white on white background → invisible at click
});
</script>

/* Detection: MutationObserver on feColorMatrix values attribute.
   Parse all 20 values on every change. Check diagonal coefficients a[0][0], a[1][1], a[2][2].
   If any diagonal is negative (e.g. -1) and the offset (a[0][4], a[1][4], a[2][4]) compensates
   to produce near-1 output for near-0 input:
     R'(-0.017) = -(-0.017)×1 + 0×(-0.017) + 0×(-0.017) + 1×1 = 0.017+1 = 1.017 → clamped 1.0
   Wait: R' = (-1)×R + 0×G + 0×B + 0×A + 1×1 = -R + 1 = 1-R. For R=0.017: R'=0.983.
   Flag: if diagonal coefficient < -0.5 AND same-row offset > 0.9 → inversion pattern. */

Attack summary

IDFilter scopeChannels invertedEffect on dark textSeverity
SA-CSS-CMI-001 Inner text span (background not included) R, G, B Dark→near-white; white background unchanged; WCAG 1.02:1 → invisible Critical
SA-CSS-CMI-002 Entire panel (background included) R, G, B Both text and bg inverted; contrast preserved (~20:1); dark-mode appearance — (true negative)
SA-CSS-CMI-003 Inner text span B only (or R only, G only) Cyan/magenta/green text; WCAG typically passes but semantics confused; background-dependent Medium
SA-CSS-CMI-004 Inner text span (mousedown) R, G, B Identity at load; near-white-on-white at click; user reads correct text but confirms invisible Critical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CMI-001: feColorMatrix type="matrix" full RGB inversion (diagonal values −1, offsets +1) on inner text element inside white-background container — dark consent text (linear≈0.017) inverts to near-white (0.983); WCAG 1.02:1 against white parent background; getComputedStyle().color unchanged (reports pre-filter color); detection requires checking which specific element carries the filter and whether its background is white.
MEDIUM SA-CSS-CMI-003: feColorMatrix type="matrix" partial inversion (single-channel negative diagonal) on consent text element — produces cyan/magenta/yellow text from near-achromatic dark input; WCAG typically passes AA on white background; primarily a semantic confusion attack; flag as MEDIUM if any single diagonal coefficient is negative on a consent text element.
CRITICAL SA-CSS-CMI-004: JS mousedown changes feColorMatrix values from identity to full inversion matrix on inner text span — static analysis sees identity (benign); click-time: near-white text on white background (invisible); MutationObserver on feColorMatrix values attribute; detect negative-diagonal pattern on every change; if filter scope is inner text span and parent background is white → CRITICAL.

Related: feColorMatrix type=matrix alpha/white-out attacks (SA-CSS-CMM)  |  feColorMatrix near-white bias forcing (SA-CSS-CMB)  |  feColorMatrix channel-swap attacks (SA-CSS-CMS)

← Blog  |  Security Checklist