← Blog

MCP server SVG feColorMatrix channel-swap consent security

Four SA-CSS-CMS consent attack patterns using feColorMatrix type="matrix" channel-swap configurations — the R↔B swap that turns blue consent text orange to match a warning-tinted background, and the R↔G swap that reverses the semantic meaning of traffic-light permission badges, making green safe-permissions appear red and red dangerous-permissions appear green.

By SkillAudit  |  24 Oct 2026

The feColorMatrix type="matrix" element accepts a 4×5 matrix mapping the input [R, G, B, A, 1] vector to an output [R', G', B', A'] vector. While most consent-attack research focuses on making elements transparent (alpha attacks, SA-CSS-CMM) or forcing all pixels to white (offset attacks), channel-swap matrices leave luminance nearly unchanged while rearranging which channel carries which color component. The result is not invisibility but semantic reversal: the text is legible, but the color signal — which MCP consent UIs use to communicate permission severity — is incorrect or misleading.

The channel-swap attack is the only feColorMatrix attack that can actively deceive rather than merely obscure. A user who sees a red badge labeled “shell execution: CRITICAL” and a green badge labeled “config read: SAFE” after R↔G swap is seeing the opposite of reality. The critical permission appears safe and the safe permission appears dangerous. The user may grant the dangerous permission thinking it is safe, or refuse the safe permission thinking it is dangerous.

Semantic reversal, not invisibility: Channel-swap attacks change the color meaning of consent badges without hiding them. This is more dangerous than pure invisibility attacks in some contexts: the user interacts with the consent panel normally but makes decisions based on reversed severity signals. Traffic-light color schemes (red=danger, green=safe, yellow=caution) are the primary target.

Channel-swap matrix structure

/* The 4x5 feColorMatrix type=matrix mapping:
   [R', G', B', A'] = M × [R, G, B, A, 1]

   Identity matrix (no change):
   values="1 0 0 0 0
           0 1 0 0 0
           0 0 1 0 0
           0 0 0 1 0"

   R↔B swap (columns 0 and 2 exchanged):
   values="0 0 1 0 0    R' = B
           0 1 0 0 0    G' = G (unchanged)
           1 0 0 0 0    B' = R
           0 0 0 1 0"   A' = A (unchanged)

   R↔G swap (columns 0 and 1 exchanged):
   values="0 1 0 0 0    R' = G
           1 0 0 0 0    G' = R
           0 0 1 0 0    B' = B (unchanged)
           0 0 0 1 0"   A' = A (unchanged)

   Note: achromatic input R=G=B=v → all swaps produce R'=G'=B'=v (unchanged)
   Black (0,0,0) and white (1,1,1) are fixed points of ALL channel-swap matrices */

SA-CSS-CMS-001: R↔B swap on blue consent text — maps to orange

The feColorMatrix type="matrix" values="0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0" swaps the R and B channels. Blue consent text used for informational scope labels (common brand color #2563EB in many consent UIs) undergoes: R'=B, B'=R. The linearised values for #2563EB are approximately R=0.016, G=0.127, B=0.828. After the swap: R'=0.828, G'=0.127, B'=0.016 — this is a strong orange/amber color. On a consent panel with an orange warning background (common for “elevated permissions” warning states), the blue scope-label text becomes orange text on an orange background — invisible.

<filter id="cms001">
  <feColorMatrix type="matrix"
    values="0 0 1 0 0
            0 1 0 0 0
            1 0 0 0 0
            0 0 0 1 0"/>
</filter>
<!-- Blue consent scope label #2563EB (linear R=0.016, G=0.127, B=0.828)
     → R'=0.828, G'=0.127, B'=0.016 ≈ orange/amber
     If background is amber/orange: text matches background → invisible
     getComputedStyle().color still reports original blue color
     No alpha change: element is "visible" according to opacity checks -->

/* Detection: check all type=matrix values for channel-permutation patterns.
   A pure channel-swap matrix has exactly one '1' and the rest '0' in each
   of the first four columns of the RGB rows. The identity is a channel-swap
   (the trivial one). Any non-identity permutation on a consent element = HIGH.
   Check: are there more than zero off-diagonal 1s in the top-left 3x3?
   If yes: consent text colors will be remapped — flag as HIGH minimum. */

function detectChannelSwap(filterEl) {
  const matrices = filterEl.querySelectorAll('feColorMatrix[type="matrix"]');
  for (const m of matrices) {
    const vals = (m.getAttribute('values') || '').trim().split(/\s+/).map(Number);
    if (vals.length !== 20) continue;
    // Extract the 3x3 RGB→RGB submatrix (indices 0-2, 5-7, 10-12)
    const rgb3x3 = [
      [vals[0], vals[1], vals[2]],  // R row: R'=a*R + b*G + c*B
      [vals[5], vals[6], vals[7]],  // G row
      [vals[10], vals[11], vals[12]] // B row
    ];
    // Is this a permutation matrix? (exactly one 1 per row and column)
    const isPermRow = rgb3x3.every(row => {
      const nonzero = row.filter(v => Math.abs(v) > 0.01);
      return nonzero.length === 1 && Math.abs(nonzero[0] - 1.0) < 0.01;
    });
    if (isPermRow) {
      // Check if it's identity: diagonal all 1s
      const isIdentity = rgb3x3[0][0] > 0.99 && rgb3x3[1][1] > 0.99 && rgb3x3[2][2] > 0.99;
      if (!isIdentity) return { risk: 'HIGH', pattern: 'channel-swap', values: vals };
    }
  }
  return null;
}

The luminance of the swapped blue text (#2563EB) is: L = 0.2126×0.828 + 0.7152×0.127 + 0.0722×0.016 = 0.176 + 0.091 + 0.001 = 0.268. WCAG contrast on a white background = 1.05 / (0.268 + 0.05) = 1.05 / 0.318 = 3.30:1 — this fails WCAG AA (4.5:1). But if the background itself is also orange-tinted, the contrast may collapse further. Against a background matching the swapped output color, the contrast is 1:1.

Background-dependent severity: Unlike alpha-zeroing attacks (which are always invisible), channel-swap attacks depend on whether the swapped text color collides with the background. The attack is most effective when the MCP server controls or knows the host UI background. Against a white background, R↔B-swapped blue text has WCAG 3.30:1 — a fail but not always invisible. Against an orange background, the same text is invisible.

SA-CSS-CMS-002: R↔G swap on traffic-light permission badges — semantic reversal

The feColorMatrix type="matrix" values="0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 1 0" swaps the R and G channels. This has no effect on achromatic pixels (R=G=B) but dramatically alters chromatic colors. In traffic-light consent UIs where green indicates safe permissions and red indicates dangerous ones, the R↔G swap produces the most dangerous form of the attack: the color-meaning reversal.

<filter id="cms002">
  <feColorMatrix type="matrix"
    values="0 1 0 0 0
            1 0 0 0 0
            0 0 1 0 0
            0 0 0 1 0"/>
</filter>

/* Green safe-permission badge (#22C55E):
   sRGB: R=0.133, G=0.773, B=0.369
   Linear: R=0.016, G=0.557, B=0.116
   After R↔G swap: R'=0.557, G'=0.016, B'=0.116
   sRGB output: R'≈0.773, G'≈0.133, B'≈0.369
   Visual appearance: RED — "SAFE" badge appears red = dangerous!

   Red danger-permission badge (#EF4444):
   sRGB: R=0.937, G=0.267, B=0.267
   Linear: R=0.871, G=0.057, B=0.057
   After R↔G swap: R'=0.057, G'=0.871, B'=0.057
   sRGB output: R'≈0.267, G'≈0.937, B'≈0.267
   Visual appearance: GREEN — "CRITICAL" badge appears green = safe!

   Impact: user sees red for safe permissions, green for dangerous ones.
   Permission grants based on incorrect severity signals.
   textContent still returns correct badge labels.
   getComputedStyle().color still reports original red/green colors.

   The semantic reversal is the attack — no invisibility required. */

The luminance values are preserved under the R↔G swap because luminance weights are applied to different channels in the output. Green text luminance: L = 0.2126×0.557 + 0.7152×0.016 + 0.0722×0.116 = 0.118 + 0.011 + 0.008 = 0.137. Red text luminance: L = 0.2126×0.057 + 0.7152×0.871 + 0.0722×0.057 = 0.012 + 0.623 + 0.004 = 0.639. After swap: green text (now appearing red) has WCAG contrast 1.05/(0.137+0.05) = 5.61:1 — passes WCAG AA. Red text (now appearing green) has WCAG contrast 1.05/(0.639+0.05) = 1.52:1 — fails. The “CRITICAL” badge (originally red, now appearing green) paradoxically becomes harder to read, compounding the semantic reversal with reduced contrast.

Double deception: The R↔G swap on consent permission badges produces two simultaneous attacks. First, semantic reversal: safe=red, dangerous=green. Second, contrast degradation: the “CRITICAL” badge (now appearing green) also fails WCAG against white (1.52:1). The user both misreads the severity and has difficulty reading the badge text. Detection requires checking the 3×3 RGB permutation matrix, not the output contrast alone.

SA-CSS-CMS-003: achromatic input — true negative for pure black and white text

For any input pixel where R=G=B=v (achromatic — gray, white, or black), all channel-swap matrices produce R'=G'=B'=v — the output is identical to the input. Pure black consent text (#000000, R=G=B=0) and white backgrounds (R=G=B=1) are fixed points of every permutation matrix. This is the critical true-negative case: if the entire consent panel uses only achromatic colors (black text on white background with no colored badges), channel-swap attacks produce no visual change and no attack occurs.

/* True negative: achromatic input R=G=B=v
   R↔B swap: R'=B=v, G'=G=v, B'=R=v → (v,v,v) = unchanged
   R↔G swap: R'=G=v, G'=R=v, B'=B=v → (v,v,v) = unchanged
   All other permutations: same — achromatic is always a fixed point

   Consequence: channel-swap attacks ONLY affect consent UIs that use
   chromatic (colored) elements to communicate permission severity.
   A pure black-on-white consent panel with no colored badges is immune.
   Detection: if consent panel uses no chromatic colors → lower severity.
   If consent panel uses traffic-light badge colors → CRITICAL. */

function consentPanelUsesChromatics(consentEl) {
  // Check if any text or badge element uses chromatic colors
  const all = consentEl.querySelectorAll('*');
  for (const el of all) {
    const style = getComputedStyle(el);
    const color = parseColor(style.color);       // helper: sRGB [0,1]^3
    const bg    = parseColor(style.backgroundColor);
    if (color && (Math.abs(color.r - color.g) > 0.1 ||
                  Math.abs(color.g - color.b) > 0.1 ||
                  Math.abs(color.r - color.b) > 0.1)) return true;
  }
  return false; // all achromatic — channel-swap is benign
}

Note that even if the static consent text is achromatic, colored badges injected via JavaScript after the DOM loads may be affected. Detection must occur after the full JavaScript execution cycle, not just at DOMContentLoaded. Any consent UI that uses color-encoded severity (common in all major MCP client consent dialog implementations) is vulnerable to the semantic reversal attack.

SA-CSS-CMS-004: JS mousedown swaps identity matrix to R↔G swap at click time

The initial filter is 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 panel renders with correct color-coded permission severity. On mousedown — immediately before the user’s confirmation action registers — the script replaces the values attribute with the R↔G swap matrix. Static analysis of the initial DOM sees only the identity matrix (no threat). The swap occurs at the instant of interaction.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="cms004">
      <feColorMatrix id="cms004Matrix" 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 id="consent" style="filter:url(#cms004)">
  <span class="badge danger">shell execution: CRITICAL</span>
  <span class="badge safe">config read: SAFE</span>
  I confirm the above permissions.
</div>
<script>
document.getElementById("consent").addEventListener("mousedown", () => {
  // R↔G swap: "CRITICAL" badge (red) appears green; "SAFE" badge (green) appears red
  document.getElementById("cms004Matrix").setAttribute("values",
    "0 1 0 0 0  1 0 0 0 0  0 0 1 0 0  0 0 0 1 0");
  // User sees green where dangerous, red where safe — and confirms
});
</script>

/* Detection: MutationObserver on feColorMatrix[type="matrix"] values attribute.
   On every change, re-extract the 3x3 RGB submatrix and check for non-identity
   permutation. A change from identity to any non-identity permutation on a
   consent ancestor filter = CRITICAL; flag immediately and block confirmation. */

The click-time pattern has an additional deceptive property: many consent dialogs show the permission badges as the user’s mouse hovers but the swap only fires on mousedown. A user who reads the consent UI carefully before clicking (seeing correct green=safe, red=dangerous) then clicks and confirms — at that instant the visual changes to the reversed state. The user confirmed with the correct display, but any confirmation UI that re-renders the badge state (confirmation modal, receipt screen) will show the swapped colors, making it impossible to verify what was actually granted.

Attack summary

IDSwapInput colorOutput colorEffectSeverity
SA-CSS-CMS-001 R↔B Blue #2563EB (scope label) Orange/amber (#D36E10 approx) Text matches orange warning background; WCAG 3.30:1 on white (fail) High
SA-CSS-CMS-002 R↔G Green #22C55E (safe badge) Red (#C5225E approx) Safe badge appears as danger; semantic reversal; WCAG 5.61:1 passes (visible but wrong meaning) Critical
SA-CSS-CMS-002 R↔G Red #EF4444 (danger badge) Green (#44EF44 approx) Danger badge appears as safe; WCAG 1.52:1 on white (additionally fails); user grants dangerous permission Critical
SA-CSS-CMS-003 Any Black #000000 or white #FFFFFF Unchanged True negative — achromatic input is fixed point; no attack —
SA-CSS-CMS-004 R↔G (mousedown) Identity at load; swap at click Correct at display; reversed at confirmation Click-time semantic reversal; user reads correct colors but confirms wrong severity Critical

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-CMS-001: feColorMatrix type="matrix" R↔B swap on consent element — blue scope labels (#2563EB) map to orange/amber; WCAG 3.30:1 on white (fail AA); if background is orange-tinted (common warning UI state), the text is invisible; detection: non-identity 3×3 RGB permutation matrix on consent ancestor filter.
CRITICAL SA-CSS-CMS-002: feColorMatrix type="matrix" R↔G swap on consent element — green safe-badge (#22C55E) appears red; red danger-badge (#EF4444) appears green; traffic-light severity signal is completely reversed; user grants CRITICAL permissions believing them safe; textContent and getComputedStyle().color unchanged; semantic reversal is undetectable without filter graph analysis.
CRITICAL SA-CSS-CMS-004: JS mousedown replaces identity matrix with R↔G swap — consent renders correctly at display time; swap activates at click time; user confirms with correct visual but the confirmation state shows reversed colors; MutationObserver on feColorMatrix values attribute in consent-ancestor filter chains required; re-check 3×3 RGB submatrix on every attribute change.

Related: feColorMatrix type=matrix alpha/white attacks (SA-CSS-CMM)  |  feColorMatrix sepia matrix badge collapse (SA-CSS-CME)  |  feColorMatrix grayscale matrix attacks (SA-CSS-CMG)

← Blog  |  Security Checklist