← Blog

MCP server SVG feColorMatrix hueRotate consent security

Four SA-CSS-CMH consent attack patterns using feColorMatrix type="hueRotate" — the SVG per-pixel hue rotation matrix that shifts all color hues by a fixed angle, exploited to rotate saturated consent text to a hue with lower WCAG contrast, neutralize color-coded warning signals, and evade CSSOM-based detection via the getComputedStyle().color blind spot.

By SkillAudit  |  24 Oct 2026

The feColorMatrix SVG filter primitive with type="hueRotate" and values="θ" applies a 3×3 hue-rotation matrix to the R, G, B channels of every pixel (SVG §15.10). The matrix is defined by the SVG specification as a combination of a luminance-preserving component (the "desaturated" portion) and a hue-rotating component (the cos(θ) and sin(θ) terms). Unlike CSS filter: hue-rotate(θ) — which is a CSS property reflected in computed styles — the SVG feColorMatrix hue rotation is applied in the SVG filter effect graph, not in the CSS property model. This makes it invisible to getComputedStyle().

The SVG hue-rotate matrix (per spec §15.10) for angle θ in degrees:

| R' |   | a  b  c | | R |
| G' | = | d  e  f | | G |
| B' |   | g  h  i | | B |

a = 0.213 + cos(θ)×0.787 − sin(θ)×0.213
b = 0.715 − cos(θ)×0.715 − sin(θ)×0.715
c = 0.072 − cos(θ)×0.072 + sin(θ)×0.928
d = 0.213 − cos(θ)×0.213 + sin(θ)×0.143
e = 0.715 + cos(θ)×0.285 + sin(θ)×0.140
f = 0.072 − cos(θ)×0.072 − sin(θ)×0.283
g = 0.213 − cos(θ)×0.213 − sin(θ)×0.787
h = 0.715 − cos(θ)×0.715 + sin(θ)×0.715
i = 0.072 + cos(θ)×0.928 + sin(θ)×0.072

For achromatic pixels (R=G=B), the matrix output is R'=G'=B'=R (the matrix preserves achromatic values). For saturated pixels, the matrix redistributes channel weights, changing the hue and potentially changing the relative luminance (Y = 0.2126R + 0.7152G + 0.0722B). Because the luminance weights are not equal, hue rotation changes luminance for saturated colors even though the matrix is designed to approximate luminance-preservation.

The CSSOM blind spot: getComputedStyle(element).color returns the logical CSS color value of the element — the value before any SVG filter transformation is applied. An auditor reading this value sees the original color and computes WCAG contrast against the original. The feColorMatrix hue rotation applies at render time in the filter graph; the computed contrast value is wrong for any saturated color that changes luminance under the rotation.

SA-CSS-CMH-001: feColorMatrix type="hueRotate" values="180" on saturated blue consent text

Blue is the most common saturated color in consent UI text — brand blues, link colors, action-item highlights. The hue-rotate matrix at θ=180° has cos(180°)=−1, sin(180°)=0. This simplifies the matrix to:

<filter id="hueRotate180">
  <feColorMatrix type="hueRotate" values="180"/>
</filter>
<!-- θ=180°: cos=-1, sin=0 -->
<!-- a=0.213+(-1)×0.787=0.213-0.787=-0.574 (negative → clamp interactions) -->
<!-- approximate effect: blue (#2563eb, rgb(37,99,235)) → warm amber/orange hue -->
<!-- original luminance ≈ 0.162; rotated luminance changes based on R'G'B' redistribution -->
<!-- getComputedStyle().color reports #2563eb; WCAG tool computes original contrast -->

For brand blue #2563eb (R=37, G=99, B=235) under θ=180°, the matrix produces significant R and B channel swaps with G remaining. The resulting color shifts into the amber/warm spectrum. The post-rotation WCAG contrast against white depends on the exact output luminance — for many common blues, the rotated luminance rises (because blue contributes only 0.0722 to luminance, while the rotated channels have higher luminance weights), which can paradoxically make the text brighter and thus potentially lower-contrast against white. The attack surface is not guaranteed contrast failure but guaranteed CSSOM evasion: every tool relying on getComputedStyle().color misses the actual rendered color.

SA-CSS-CMH-002: feColorMatrix type="hueRotate" values="90" on red warning text

Red (#dc2626) is used in consent dialogs for warning annotations, high-risk permission notes, and irreversible-action callouts. Hue-rotating red by 90° shifts it toward lime-yellow-green. The perceptual significance of "red = warning" is destroyed: a lime-green note on a white background carries no urgency signal. Even if the technical WCAG contrast ratio passes, the semantic color coding fails.

<filter id="hueRotate90">
  <feColorMatrix type="hueRotate" values="90"/>
</filter>
<!-- Red #dc2626 (hue ~0°) → rotated to ~90° hue (yellow-green) -->
<!-- Warning callout text appears as lime-yellow; red=danger signal neutralized -->
<!-- Security tool reads computedStyle red color; reports red warning present -->
<!-- Rendered UI shows lime-green text; warning significance lost -->

Semantic color attack: WCAG contrast is a luminance-based metric that does not account for semantic color use. A red warning that passes contrast check in a WCAG audit may be rendered as lime-green by feColorMatrix hueRotate. Screen readers are unaffected (they read text content), but sighted users lose the "this is a warning" color cue. The attack targets color-coded risk disclosure in consent dialogs specifically — the red text that says "This action cannot be undone" or "Grants access to all files".

SA-CSS-CMH-003: feColorMatrix type="hueRotate" on entire consent container (text and background rotate together)

When the filter is applied to the consent container element rather than to individual text nodes, both the text color and the background color rotate simultaneously. For achromatic backgrounds (white #ffffff, near-white #f9fafb): achromatic pixels have R=G=B and the hue-rotate matrix preserves achromatic values (R'=G'=B'=R). The background remains white — only the text (which may be a saturated brand color) shifts hue.

<filter id="hueRotateContainer">
  <feColorMatrix type="hueRotate" values="180"/>
</filter>
<div style="filter:url(#hueRotateContainer)">
  <!-- Entire consent dialog: both text and background filtered -->
  <!-- Achromatic white background: hueRotate preserves achromatic colors -->
  <!-- Saturated text (brand blue, warning red): hue and luminance shift -->
  <!-- getComputedStyle().color on child text elements: unchanged -->
</div>

When the background is achromatic and the text is saturated, the filter selectively changes the text color while leaving the background unchanged. This means the relative contrast between text and background can change — and in the attacker's favor, it can drop below WCAG thresholds if the saturation and hue of the text color are chosen to minimize post-rotation contrast. The attack requires knowledge of the target UI's text and background color pair; an MCP server can query these at install time via getComputedStyle() before applying the filter.

SA-CSS-CMH-004: JS mousedown sets feColorMatrix values from "0" to "180" (click-time hue rotation)

The initial state uses values="0" — a rotation of 0° is the identity transformation (R'=R, G'=G, B'=B). The consent element renders with original colors. On mousedown, the script sets values to "180", activating the hue rotation at click time. All detection tools evaluating the initial render state see the original colors.

consentElement.addEventListener("mousedown", () => {
  const matrix = document.getElementById("hueFilter")
                           .querySelector("feColorMatrix");
  matrix.setAttribute("values", "180"); // 0° identity → 180° hue rotation
  // colors shift; getComputedStyle().color still reports original
  // visual rendering changes at instant of click
});

The values="0" initial attribute is unambiguously a no-op — any static analysis tool correctly identifies this as identity. The attack is entirely in the dynamic attribute mutation. MutationObserver on the values attribute of feColorMatrix elements with type="hueRotate" within consent ancestor filters is the required runtime defence. The observer callback should re-evaluate the post-rotation contrast of the consent element whenever values changes from 0 to any non-zero value.

Attack summary

IDvaluesTargetEffectSeverity
SA-CSS-CMH-001180°Brand blue textHue shifts to amber; CSSOM blind spot on contrastHigh
SA-CSS-CMH-00290°Red warning textRed→lime; semantic color-coding destroyedHigh
SA-CSS-CMH-003180°Entire consent containerAchromatic bg preserved; saturated text shiftsMedium
SA-CSS-CMH-0040°→180° mousedownAny consent textIdentity at load; hue-rotated at clickHigh

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-CMH-001: feColorMatrix type="hueRotate" values="180" on saturated blue consent text — SVG hue-rotate matrix; blue hue (~230°) rotated to warm spectrum; output luminance changes; rendered WCAG contrast differs from CSSOM-derived value; getComputedStyle().color blind spot; detection: apply SVG §15.10 matrix to expected text RGB; recompute contrast vs background.
HIGH SA-CSS-CMH-002: feColorMatrix type="hueRotate" values="90" on red warning text — red (hue ~0°) → lime-yellow-green (hue ~90°); warning color significance lost; WCAG contrast check may pass numerically while semantic red=danger signal is destroyed; detect hue-range shifts on red-hue consent text: flag hueRotate on colors with hue in 330°–30° range.
MEDIUM SA-CSS-CMH-003: feColorMatrix type="hueRotate" on entire consent container — achromatic backgrounds (white, near-white) preserved by hue rotation (achromatic invariant); saturated text color changes; differential contrast shift; getComputedStyle() on child text elements still reports original; apply matrix to each distinct text color in the consent subtree.
HIGH SA-CSS-CMH-004: JS mousedown sets feColorMatrix values from "0" (identity) to non-zero — initial values="0" is identity transformation; static analysis benign; click-time rotation activates; getComputedStyle unchanged throughout; MutationObserver on feColorMatrix type=hueRotate values attribute within consent ancestor filter required.

Related: feColorMatrix type=saturate attacks  |  feColorMatrix type=matrix attacks  |  CSS filter hue-rotate attacks

← Blog  |  Security Checklist