MCP server SVG feColorMatrix mixed alpha+RGB consent security
Four SA-CSS-CMA consent attack patterns using a single feColorMatrix type="matrix" to simultaneously manipulate both RGB colour channels and the alpha channel — combining colour distortion (sepia, channel-swap) with transparency reduction to produce consent text that is invisible against white backgrounds after alpha compositing, while no individual axis of the attack triggers a single-axis detection rule.
The feColorMatrix type="matrix" 4×5 matrix allows independent specification of all four output channels (R′, G′, B′, A′). Single-axis attacks modify either the RGB rows (e.g., sepia colour distortion keeping A′=A) or the alpha row (e.g., reducing opacity while keeping colours). The combined attack sets both: the RGB rows apply colour distortion and the alpha row scales or zeroes the output alpha. The resulting pixel is colour-distorted AND semi-transparent. When composited against a white background, the effective luminance after alpha-blending can be close to 1.0 even when neither the colour distortion nor the alpha reduction alone would achieve invisibility.
Composite-channel detection gap: A scanner that checks only RGB output rows may detect the colour distortion but accept the matrix because the text “still has contrast” — ignoring that 80% transparency composited on white background makes that contrast irrelevant. A scanner checking only the alpha row may see “0.2× scaling” and flag it, but only if alpha reduction is in scope. The combined attack requires both the alpha-compositing calculation and the colour-distortion check to be integrated. Single-axis tools miss it.
Alpha-compositing formula for semi-transparent filter output on white background
/* When feColorMatrix produces output with alpha A' ∈ (0, 1):
The filtered element composites over the white background (L=1.0) using
Porter-Duff "source over":
L_effective = A' × L_filtered + (1 - A') × L_background
= A' × L_filtered + (1 - A') × 1.0
WCAG contrast of consent element vs white:
contrast = (1.0 + 0.05) / (L_effective + 0.05)
For SA-CSS-CMA-001 (sepia + A'=0.2×A):
Dark text after sepia: L_sepia ≈ 0.35 (brownish gray)
A' = 0.2 × 1.0 = 0.2 (for opaque input text)
L_eff = 0.2 × 0.35 + 0.8 × 1.0 = 0.07 + 0.80 = 0.87
WCAG = 1.05 / (0.87 + 0.05) = 1.05 / 0.92 = 1.14:1 — INVISIBLE
Key insight: even if L_filtered = 0 (pure black after RGB transform):
L_eff = 0.2 × 0.0 + 0.8 = 0.80
WCAG = 1.05 / 0.85 = 1.24:1 — still invisible
Any A' ≤ 0.2 on a white background makes text effectively invisible,
regardless of what the RGB channels are doing. */
SA-CSS-CMA-001: sepia RGB rows combined with 80% alpha reduction
The standard sepia matrix is applied to RGB rows and the alpha output row is modified to scale A by 0.2: values="0.393 0.769 0.189 0 0 0.349 0.686 0.168 0 0 0.272 0.534 0.131 0 0 0 0 0 0.2 0". For opaque dark consent text (#111111, linearRGB ≈ 0.017): sepia output is warm brown (R′≈0.026, G′≈0.023, B′≈0.018), L_sepia≈0.024; A′=0.2. Composited on white: L_eff=0.2×0.024+0.8=0.805; WCAG=1.05/0.855=1.23:1. For lighter text or consent panel sections with mid-gray text (#6b7280, linear≈0.17): sepia L≈0.18; L_eff=0.2×0.18+0.8=0.836; WCAG=1.05/0.886=1.19:1. Both are invisible. The sepia distortion provides colour-misdirection while the alpha reduction provides the compositing invisibility.
<filter id="cma001">
<feColorMatrix type="matrix"
values="0.393 0.769 0.189 0 0
0.349 0.686 0.168 0 0
0.272 0.534 0.131 0 0
0 0 0 0.2 0"/>
</filter>
<!-- Sepia rows (standard ITU sepia coefficients) applied to R,G,B output.
Alpha row: A' = 0.2 × A (80% transparency reduction)
Dark text #111111 (linear ≈ 0.017):
After sepia: brownish L_sepia ≈ 0.024
After alpha: A' = 0.2
Composited on white: L_eff = 0.2×0.024 + 0.8 = 0.805
WCAG = 1.05/0.855 = 1.23:1 — invisible
Single-axis check:
RGB rows only: sepia is SUSPICIOUS (both colours wrong AND low luminance)
Alpha row only: 0.2× scaling is suspicious
But the attack combines both: a tool that flags "suspicious sepia" may
not integrate the alpha-compositing calculation, and vice versa. -->
SA-CSS-CMA-002: all-zero 20-element matrix — fully transparent output
Setting all 20 values to zero: values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0". Every output channel (including A′) is zero. All pixels become fully transparent. The consent panel disappears from the rendering tree entirely — more complete than a near-white bias attack (which retains opaque pixels). Unlike CSS opacity: 0 (reflected in computed styles), this filter transformation is invisible to getComputedStyle(). The element still occupies layout space and its click target remains active, so the user can “confirm” an invisible consent dialog.
<filter id="cma002">
<feColorMatrix type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0"/>
</filter>
<!-- R' = G' = B' = A' = 0 for every pixel.
All pixels fully transparent → entire filtered element invisible.
CSS opacity: 1 — getComputedStyle().opacity = "1" (no flag)
Element layout box still present — click target still works.
Detection: check if A output row produces zero for any input:
if v[15]=v[16]=v[17]=v[18]=0 AND v[19]=0 → fully transparent output
This is a special case of alpha row zeroing (v[18]=0, all others zero too).
More general: if max(A' for all test input pixels) ≈ 0 → invisible. -->
SA-CSS-CMA-003: B↔R channel swap combined with 50% alpha reduction
RGB channel swap (B→R, R→B, G preserved) combined with 50% alpha scale: values="0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0.5 0". For blue consent text #2563EB (linearRGB R=0.016, G=0.127, B=0.828): after swap → (0.828, 0.127, 0.016) orange-red; L=0.2126×0.828+0.7152×0.127+0.0722×0.016≈0.268; A′=0.5. Composited on white: L_eff=0.5×0.268+0.5=0.634; WCAG=1.05/0.684=1.54:1 — fail. The colour swap destroys semantic meaning (blue links become orange) and the alpha reduction makes contrast fail on compositing. Both are necessary: channel swap alone at full opacity gives WCAG≈3.83:1 (borderline); alpha reduction alone at 50% gives L_eff=0.5×0.162+0.5=0.581, WCAG=1.81:1 (fail but less deceptive looking); the combination is 1.54:1 — lower than either alone.
<filter id="cma003">
<feColorMatrix type="matrix"
values="0 0 1 0 0
0 1 0 0 0
1 0 0 0 0
0 0 0 0.5 0"/>
</filter>
<!-- R' = B (blue → red channel), G' = G (preserved), B' = R (red → blue channel)
A' = 0.5 × A (50% transparency)
Blue #2563EB (R=0.016, G=0.127, B=0.828, A=1):
After swap: (0.828, 0.127, 0.016) → orange-red
A' = 0.5
Composite on white: L_eff = 0.5×0.268 + 0.5 = 0.634
WCAG = 1.05/0.684 = 1.54:1 — fail
Green safe-badge #22C55E (R=0.016, G=0.424, B=0.085):
After B↔R swap: (0.085, 0.424, 0.016) → yellow-green
L_swap = 0.2126×0.085 + 0.7152×0.424 + 0.0722×0.016 ≈ 0.321
A' = 0.5
Composite: L_eff = 0.5×0.321 + 0.5 = 0.661
WCAG = 1.05/0.711 = 1.48:1 — fail
Both blue text and green safe-badge fail; channel meaning destroyed. -->
SA-CSS-CMA-004: JS mousedown identity → sepia+alpha-reduction at click time
The initial filter is the identity matrix. Consent text is fully visible. On mousedown, the matrix is atomically replaced with the sepia-plus-opacity-reduction variant. The consent panel instantly becomes near-invisible (WCAG 1.23:1) at the precise moment the user taps the confirmation button. Static page analysis sees the identity matrix and reports no finding. The attack requires only a single DOM attribute mutation observed by a MutationObserver on the feColorMatrix element.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="cma004">
<feColorMatrix id="cma004M" 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(#cma004)">
I authorise network egress from this MCP server.
<button id="allow">Allow</button>
</div>
<script>
document.getElementById("allow").addEventListener("mousedown", () => {
document.getElementById("cma004M").setAttribute("values",
"0.393 0.769 0.189 0 0 0.349 0.686 0.168 0 0 0.272 0.534 0.131 0 0 0 0 0 0.2 0");
// Sepia + 80% opacity reduction: L_eff≈0.81 on white; WCAG≈1.23:1
});
</script>
Attack summary
| ID | RGB manipulation | Alpha manipulation | L_eff on white | WCAG | Severity |
|---|---|---|---|---|---|
| SA-CSS-CMA-001 | Sepia (brown-gray) | A′=0.2×A (80% reduction) | ≈0.87 | 1.14:1 | Critical |
| SA-CSS-CMA-002 | All zeros (forced black) | A′=0 (fully transparent) | — (invisible) | — | Critical |
| SA-CSS-CMA-003 | B↔R channel swap (orange cast) | A′=0.5×A (50% reduction) | ≈0.63 | 1.54:1 | Critical |
| SA-CSS-CMA-004 | Identity→sepia (mousedown) | Identity→0.2× (mousedown) | — | — | Critical |
Defences
- Integrate alpha-compositing into contrast calculation: When a feColorMatrix has a non-identity alpha output row (v[18] ≠ 1 or v[19] ≠ 0), compute the effective luminance after alpha-compositing over white: L_eff = A_output×L_filtered + (1−A_output)×1.0. Use L_eff (not L_filtered) in the WCAG formula. Any alpha output scale below 0.3 on a white background renders text invisible regardless of the RGB channel values.
- Flag alpha-row zero as a separate critical rule: A feColorMatrix where v[15]=v[16]=v[17]=v[18]=0 and v[19]=0 (fully transparent output row) must be flagged CRITICAL — the entire filtered element becomes invisible. This is independent of what the RGB rows do.
- Check alpha output row independently from RGB rows: The alpha output coefficients (positions v[15]–v[19]) should be checked alongside the RGB row analysis. Any alpha output that reduces opacity below 0.5 for opaque input content requires alpha-compositing evaluation.
- MutationObserver on feColorMatrix values attribute: Re-run integrated alpha+RGB analysis on every attribute change. The SA-CSS-CMA-004 identity→combined-attack transition fires at mousedown. Block the confirmation event if the updated matrix produces invisible output on alpha-compositing evaluation.
SkillAudit findings for this attack surface
feColorMatrix type="matrix" combining sepia RGB rows with alpha output row scaled to 0.2 (v[18]=0.2); dark consent text: sepia-brown colour output at 20% opacity composited on white → L_eff≈0.87; WCAG 1.14:1 (invisible); single-axis tools checking only RGB rows may flag sepia but not evaluate composited contrast; single-axis tools checking only alpha reduction may not check actual contrast result; integrated alpha-compositing calculation required.feColorMatrix type="matrix" all-zero 20-element matrix (values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0'); every output channel including A' is forced to zero; entire filtered element fully transparent; CSS opacity and getComputedStyle() report no change; element layout and click target remain active — user can confirm invisible consent dialog; detection: check if A output row produces zero for opaque input (v[18]=0 and all zero → A'=0).feColorMatrix type="matrix" combining B↔R channel swap (semantic destruction) with 50% alpha scale (v[18]=0.5); blue consent text #2563EB → orange-red at 50% opacity on white → L_eff=0.634; WCAG 1.54:1 (fail); green safe-badge #22C55E → yellow-green at 50% opacity → L_eff=0.661; WCAG 1.48:1 (fail); both colour semantics and contrast fail simultaneously.