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.
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
| ID | Swap | Input color | Output color | Effect | Severity |
|---|---|---|---|---|---|
| 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
- Extract and check the 3×3 RGB submatrix of every
type="matrix"feColorMatrix on consent ancestors: Parse the 20 values, extract indices [0–2], [5–7], [10–12] as the three R/G/B output rows. If the submatrix is a non-identity permutation matrix (exactly one 1 per row/column, all others near zero), flag as HIGH if the consent panel is achromatic and CRITICAL if the consent panel uses chromatic severity badges. - Detect non-permutation cross-channel mixing: The permutation check catches pure-swap matrices. Arbitrary cross-channel coefficients (e.g. R’=0.5R+0.5B) are also attacks. Check whether the top-left 3×3 submatrix equals the identity; if not, evaluate the output color of each identified consent badge color and check WCAG contrast and color meaning against expected badge semantics.
- Map consent badge colors to expected semantics: Parse consent badge background/text colors; classify into danger (high R, low G/B), safe (high G, low R/B), caution (high R+G, low B) categories. After applying the feColorMatrix transform, re-classify the output colors. If the semantic category changed (safe→danger or danger→safe), flag as CRITICAL regardless of WCAG contrast result.
- MutationObserver on
valuesattribute of all feColorMatrix elements in consent filter graphs: Re-run the permutation check on every attribute change. An identity→non-identity transition at the time of user interaction is the SA-CSS-CMS-004 click-time pattern.
SkillAudit findings for this attack surface
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.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.