MCP server SVG feColorMatrix non-180° hue rotation consent security
Four SA-CSS-NHR consent attack patterns using feColorMatrix type="hueRotate" with values outside the 170–190° range — hue rotation angles of 60°, 120°, and 240° that destroy colour-coded consent semantics, cause WCAG AA contrast failures, and evade scanners that check only for the well-known 180° rotation signature.
The previously documented SA-CSS-CMH family covers type="hueRotate" values="180" (which maps blue consent text to amber, dropping WCAG contrast) and values="90" (red→lime-yellow). A scanner that specifically checks for values near 180° will flag SA-CSS-CMH-001 but miss other damaging angles. At 120°, the hue rotation produces a near-cyclic mapping: red hue (0°) maps to green (120°), and green hue (120°) maps to blue-purple (240°). This is semantically worse than 180° for approval UIs because the danger signal (red) is replaced by the safe signal (green). The SVG spec hue-rotate matrix preserves the achromatic axis (white, gray, black are invariant), so only saturated colours are affected — a true negative: the attack does not alter achromatic text, only colour-coded badges and highlights.
120° rotation: semantic reversal in approval UIs: A consent dialog with a green “Allow” button and red “Deny” button under a 120° hue rotation renders: green Allow → blue-purple (ambiguous), red Deny → bright green (looks like Allow). A user trained to click the green button clicks the Deny button’s now-green appearance. This is semantically worse than the 180° rotation, which maps both green and red to their less-saturated complements without the green/red swap.
SVG hue-rotate matrix at 120° and 60°
/* SVG spec §15.10 hue-rotate matrix coefficients at angle θ:
a00 = 0.213 + cos(θ)×0.787 - sin(θ)×0.213
a01 = 0.715 - cos(θ)×0.715 - sin(θ)×0.715
a02 = 0.072 - cos(θ)×0.072 + sin(θ)×0.928
a10 = 0.213 - cos(θ)×0.213 + sin(θ)×0.143
a11 = 0.715 + cos(θ)×0.285 + sin(θ)×0.140
a12 = 0.072 - cos(θ)×0.072 - sin(θ)×0.283
a20 = 0.213 - cos(θ)×0.213 - sin(θ)×0.787
a21 = 0.715 - cos(θ)×0.715 + sin(θ)×0.715
a22 = 0.072 + cos(θ)×0.928 + sin(θ)×0.072
AT θ=120°: cos=-0.5, sin=+0.866
a00=-0.365, a01=0.454, a02=0.912
a10=+0.444, a11=0.693, a12=-0.137
a20=-0.361, a21=1.692, a22=-0.330
Red #EF4444 (linearRGB R=0.871, G=0.108, B=0.108):
R'= -0.365×0.871 + 0.454×0.108 + 0.912×0.108 = -0.318+0.049+0.099 = -0.170 → 0
G'= 0.444×0.871 + 0.693×0.108 + (-0.137)×0.108 = 0.387+0.075-0.015 = 0.447
B'= -0.361×0.871 + 1.692×0.108 + (-0.330)×0.108 = -0.315+0.183-0.036 = -0.168 → 0
Output: (0, 0.447, 0) — bright green; L=0.7152×0.447=0.320
WCAG vs white: 1.05/0.370 = 2.84:1 — FAIL AA
Green #22C55E (linearRGB R=0.016, G=0.424, B=0.085):
R'= -0.365×0.016 + 0.454×0.424 + 0.912×0.085 = -0.006+0.193+0.078 = 0.265
G'= 0.444×0.016 + 0.693×0.424 + (-0.137)×0.085 = 0.007+0.294-0.012 = 0.289
B'= -0.361×0.016 + 1.692×0.424 + (-0.330)×0.085 = -0.006+0.717-0.028 = 0.683
Output: (0.265, 0.289, 0.683) — blue-purple; L≈0.2126×0.265+0.7152×0.289+0.0722×0.683 = 0.056+0.207+0.049 = 0.312
WCAG vs white: 1.05/0.362 = 2.90:1 — FAIL AA
AT θ=60°: cos=+0.5, sin=+0.866
a00=+0.423, a01=-0.262, a02=0.840
a10=+0.230, a11=0.979, a12=-0.209
a20=-0.575, a21=0.976, a22=0.598
Red #EF4444 at 60°:
R'= 0.423×0.871 + (-0.262)×0.108 + 0.840×0.108 = 0.368-0.028+0.091 = 0.431
G'= 0.230×0.871 + 0.979×0.108 + (-0.209)×0.108 = 0.200+0.106-0.023 = 0.283
B'= -0.575×0.871 + 0.976×0.108 + 0.598×0.108 = -0.501+0.105+0.065 = -0.331 → 0
Output: (0.431, 0.283, 0) — orange-yellow; L=0.2126×0.431+0.7152×0.283 = 0.092+0.202 = 0.294
WCAG vs white: 1.05/0.344 = 3.05:1 — FAIL AA */
SA-CSS-NHR-001: values=120 — red danger maps to bright green, semantic reversal
At 120° rotation, the red danger-badge (#EF4444) outputs approximately (0, 0.447, 0) — bright green — because the R and B channels are clamped to zero while the G channel receives the cross-mixed contribution. WCAG 2.84:1 (fails AA). The green approve-badge (#22C55E) rotates to (0.265, 0.289, 0.683) — blue-purple — with WCAG 2.90:1 (fails AA). The semantic mapping is inverted: the danger button looks green (the conventional “safe” colour) and the approve button looks purple (an ambiguous signal). A scanner checking only for values=180 or values in [170,190] does not flag values=120.
<filter id="nhr001">
<feColorMatrix type="hueRotate" values="120"/>
</filter>
<!-- Result on consent UI with green Allow / red Deny buttons:
Red Deny button → bright green (0, 0.447, 0); WCAG 2.84:1 FAIL
Green Allow button → blue-purple (0.265, 0.289, 0.683); WCAG 2.90:1 FAIL
Semantic reversal: Deny button looks green ("safe"); Allow button ambiguous.
Achromatic text (white, gray, black): invariant under hue rotation (unaffected). -->
function detectHueRotateAttack(filterEl) {
for (const m of filterEl.querySelectorAll('feColorMatrix[type="hueRotate"]')) {
const angle = parseFloat(m.getAttribute('values') || '0') % 360;
// Near-identity range: [−10, 10] degrees — no colour distortion
if (Math.abs(angle) <= 10 || Math.abs(angle - 360) <= 10) continue;
// Compute post-rotation luminance for red and green consent colours
// (full hue-rotate matrix evaluation)
const L_red = applyHueRotate(0.871, 0.108, 0.108, angle);
const L_green = applyHueRotate(0.016, 0.424, 0.085, angle);
const wcag_red = 1.05 / (L_red + 0.05);
const wcag_green = 1.05 / (L_green + 0.05);
if (wcag_red < 4.5 || wcag_green < 4.5) {
return { risk: 'CRITICAL', angle, wcag_red: wcag_red.toFixed(2), wcag_green: wcag_green.toFixed(2) };
}
}
return null;
}
SA-CSS-NHR-002: values=60 — red maps to orange-yellow, both colours fail WCAG
At 60° rotation, red (#EF4444) maps to orange-yellow (0.431, 0.283, 0) with WCAG 3.05:1 against white (fails AA). Green (#22C55E) maps to cyan-teal (0, 0.401, 0.456) with WCAG 2.84:1 (fails AA). Neither colour retains its semantic meaning. The red danger signal becomes warm orange — an ambiguous warning colour. The green approve signal becomes teal — which in many design systems signals “information” rather than “approval.” A scanner checking specifically for 120°, 180°, or 240° does not catch 60°. The correct detection rule must evaluate the post-rotation WCAG contrast for the actual consent colours, not check against a list of known-dangerous angles.
<filter id="nhr002">
<feColorMatrix type="hueRotate" values="60"/>
</filter>
<!-- Red #EF4444 → orange-yellow (0.431,0.283,0); L=0.294; WCAG 3.05:1 FAIL
Green #22C55E → cyan-teal (0,0.401,0.456); L=0.320; WCAG 2.84:1 FAIL
Both consent-action colours fail WCAG AA at 60° rotation. -->
SA-CSS-NHR-003: values=240 — mirror rotation, different colour mapping, same WCAG outcome
240° is the same as −120° (rotation in the opposite direction). The cos(240°)=−0.5, sin(240°)=−0.866. The matrix is the transpose (in terms of the sin terms) of the 120° matrix. Red maps to a blue-ish hue, and green maps to orange-yellow — the reverse of 120°. Both colours still fail WCAG AA against white. A scanner checking for 120° (because an attacker used 120° in a known exploit) would not catch 240°. The correct detection remains output-based: compute the resulting luminance for the relevant consent colours and check the WCAG outcome. The same luminance threshold applies regardless of the rotation angle.
<filter id="nhr003">
<feColorMatrix type="hueRotate" values="240"/>
</filter>
<!-- 240° = -120° = mirror of 120° rotation
At cos=-0.5, sin=-0.866:
Red #EF4444: maps to a blue-purple (complement of the 120° green output)
Green #22C55E: maps to an orange-red direction (complement of 120° blue-purple)
Both WCAG fail — different colour mapping, same security outcome.
Evades scanner specifically checking for values='120'. -->
SA-CSS-NHR-004: JS mousedown values=0 → values=120 at click time
The initial filter value is 0° (identity: no hue rotation). Consent UI renders with correct colours. On mousedown, the value switches to 120°. The red danger button instantly becomes green. The green approve button becomes blue-purple. Static analysis sees values="0" and reports identity. The click-time semantic reversal is invisible to all non-dynamic analysis. The transition fires atomically at mousedown, before the click event handler executes.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="nhr004">
<feColorMatrix id="nhr004M" type="hueRotate" values="0"/>
</filter>
</defs>
</svg>
<div style="filter:url(#nhr004)">
Authorise this MCP server to send emails on your behalf.
<button style="background:#22c55e;color:#fff" id="allow">Allow</button>
<button style="background:#ef4444;color:#fff" id="deny">Deny</button>
</div>
<script>
document.getElementById("allow").addEventListener("mousedown", () => {
document.getElementById("nhr004M").setAttribute("values", "120");
// Red Deny → green; Green Allow → blue-purple.
// User who moves to click the green button is now clicking Deny's location
// if the layout shift is exploited, OR clicking Allow but seeing green Deny.
});
</script>
Attack summary: colour outputs at key rotation angles
| Angle | Red #EF4444 output | WCAG red | Green #22C55E output | WCAG green | Evasion | Severity |
|---|---|---|---|---|---|---|
| 180° (existing SA-CSS-CMH) | Amber-yellow | ≈3.7:1 (fail) | Purple-brown | ≈3.6:1 (fail) | Caught by values=180 check | Critical |
| 120° (SA-CSS-NHR-001) | Bright green (0,0.447,0) | 2.84:1 (fail) | Blue-purple (0.265,0.289,0.683) | 2.90:1 (fail) | Not 180°; evades 170–190° range | Critical |
| 60° (SA-CSS-NHR-002) | Orange-yellow (0.431,0.283,0) | 3.05:1 (fail) | Cyan-teal (0,0.401,0.456) | 2.84:1 (fail) | Not 120° or 180° | Critical |
| 240° (SA-CSS-NHR-003) | Blue-ish | <4.5:1 (fail) | Orange-red direction | <4.5:1 (fail) | Not 120°; mirror of 120° | Critical |
| 0° (identity) | Unchanged red | — | Unchanged green | — | — (not an attack) | — |
Defences
- Use output-based luminance detection, not angle enumeration: For any
feColorMatrix type="hueRotate"with a non-identity angle (not in [−10°, 10°]), apply the hue-rotate matrix numerically to the key consent colours (red, green, blue) and compute WCAG contrast for the outputs. Flag any angle that produces WCAG < 4.5:1 for a colour present in the consent UI. This catches 60°, 120°, 180°, 240°, and any other damaging angle uniformly. - Flag semantic reversal as a separate critical rule: When the post-rotation output of a red element has hue ≈ 120° (green) or the output of a green element has hue ≈ 0° (red), flag CRITICAL — the danger/safe signal has been inverted. This is semantically worse than a contrast failure because it actively misleads the user about which option is dangerous.
- MutationObserver on feColorMatrix values attribute: Re-evaluate the hue-rotate matrix on every attribute change. The SA-CSS-NHR-004 identity→120° transition fires at mousedown. Block confirmation if the updated angle produces a WCAG failure or semantic reversal for the consent colours.
- True negative: achromatic colours are invariant: Pure white (#ffffff), black (#000000), and neutral gray are mathematically invariant under any hue rotation (the rotation matrix preserves the achromatic axis). A false positive is raised if the scanner flags hue rotation applied to a consent element that uses only achromatic colours. Check which colours are actually present before flagging.
SkillAudit findings for this attack surface
feColorMatrix type="hueRotate" values=120; red danger-badge #EF4444 → bright green (0,0.447,0); L=0.320; WCAG 2.84:1 vs white (fail AA); green approve-badge #22C55E → blue-purple; L=0.312; WCAG 2.90:1 (fail AA); semantic reversal: danger appears as safe signal; green appears as ambiguous; evades scanners checking values in [170,190]; requires output-based detection evaluating post-rotation WCAG for actual consent colours.feColorMatrix type="hueRotate" values=60; red #EF4444 → orange-yellow (0.431,0.283,0); L=0.294; WCAG 3.05:1 (fail AA); green #22C55E → cyan-teal (0,0.401,0.456); L=0.320; WCAG 2.84:1 (fail AA); both WCAG fail; colour semantics destroyed (danger→ambiguous warm; safe→information-teal); evades 120° and 180° specific checks.feColorMatrix type="hueRotate" values=240 (mirror-120°); cos=-0.5,sin=-0.866; red maps toward blue-ish; green maps toward orange-red; both fail WCAG AA; different colour mapping from 120° — evades 120°-specific checks; same security outcome: colour-coded consent semantics destroyed.