Security Guide
MCP server CSS filter: hue-rotate() security — red-to-green warning inversion, permission badge color swap, status indicator confusion, and audit detection
CSS filter: hue-rotate(Ndeg) shifts every hue value in an element by N degrees around the HSL color wheel. At hue-rotate(120deg), red (#f44336) maps to green (~#36f443); at hue-rotate(240deg), green maps to red. The rotation applies simultaneously to text color, borders, backgrounds, shadows, and SVG fills — making it a whole-element semantic color attack. The filter operates on GPU pixels after layout and paint, so getComputedStyle(el).color still returns the original pre-rotation value. MCP servers exploit this gap to invert the color semantics of consent dialogs and security warnings without altering a single DOM attribute, CSS color property, or accessibility tree label.
How hue-rotate() works on the HSL color wheel
The HSL (Hue, Saturation, Lightness) model places colors on a 360-degree wheel: 0° and 360° are red, 120° is green, 240° is blue. filter: hue-rotate(Ndeg) shifts every pixel's hue by N degrees modulo 360, leaving saturation and lightness unchanged. The mapping formula is:
hue_output = (hue_input + rotation_degrees) mod 360
Concrete color mappings at common attack rotations:
| Input color | Input hue | Rotation | Output hue | Output color |
|---|---|---|---|---|
| Red #f44336 | 4° | hue-rotate(120deg) | 124° ≈ green | ~#36f443 |
| Red #f44336 | 4° | hue-rotate(180deg) | 184° ≈ cyan | ~#36f4eb |
| Red #f44336 | 4° | hue-rotate(240deg) | 244° ≈ blue | ~#3643f4 |
| Green #4caf50 | 123° | hue-rotate(240deg) | 363° mod 360 = 3° ≈ red | ~#af4c50 |
| Green #4caf50 | 123° | hue-rotate(180deg) | 303° ≈ magenta | ~#af4cab |
| Blue #2196f3 | 207° | hue-rotate(120deg) | 327° ≈ pink | ~#f321a6 |
Because the filter applies to every channel after compositing — not to individual CSS property values — getComputedStyle(el).color returns the pre-rotation value (e.g., rgb(244, 67, 54) for red), while the user sees green. This is the key audit evasion property: CSS-only audits that read computed color properties will not detect the inversion.
GPU-layer filter, not CSS property: filter is applied by the browser's compositing engine after the CSS paint phase. No CSS color property is modified. The DOM, CSSOM, and accessibility tree all reflect the original colors — only the GPU output seen by the user is changed.
Attack 1: hue-rotate(120deg) converts red security warning to green — safe-looking
A standard MCP consent UI renders a danger warning in red: the element's color is #f44336, its border-color is #f44336, and any translucent background tint is rgba(244, 67, 54, 0.1). All of these are red, communicating danger to the user. An MCP server that can inject CSS applies filter: hue-rotate(120deg) to the element. Because the rotation applies uniformly to every pixel, all three color components — text, border, background — shift from red (4°) to green (~124°). The warning now appears to be a green "OK" or "APPROVED" indicator.
/* MCP server injects — red warning → green "OK" appearance */
.security-warning {
filter: hue-rotate(120deg);
/* textContent: 'DANGER: This server requests filesystem access'
user sees: Green text that looks like an OK/approved indicator
getComputedStyle(el).color returns: rgb(244, 67, 54) ← still red
getComputedStyle(el).filter returns: hue-rotate(120deg) ← audit here */
}
The critical audit gap: a tool that reads getComputedStyle(el).color sees rgb(244, 67, 54) — red — and concludes the element is correctly styled. The user, however, sees green. Only by reading getComputedStyle(el).filter and parsing any hue-rotate() values can an auditor detect that the visual color has been rotated away from the semantic color.
// Detection: check filter property for hue-rotate on security-critical elements
function checkHueRotateOnElement(el) {
const filterValue = getComputedStyle(el).filter;
// Match hue-rotate() anywhere in the filter chain
const hueRotatePattern = /hue-rotate\(\s*([-\d.]+)(deg|rad|turn|grad)?\s*\)/i;
const match = filterValue.match(hueRotatePattern);
if (!match) return null;
let degrees = parseFloat(match[1]);
const unit = (match[2] || 'deg').toLowerCase();
// Normalize to degrees
if (unit === 'rad') degrees = degrees * (180 / Math.PI);
if (unit === 'turn') degrees = degrees * 360;
if (unit === 'grad') degrees = degrees * 0.9;
degrees = ((degrees % 360) + 360) % 360; // normalize to 0–359
if (degrees > 5) {
// Any non-trivial rotation on a security element is a finding
return {
element: el,
filterRaw: filterValue,
rotationDeg: degrees,
computedColor: getComputedStyle(el).color, // original pre-rotation color
finding: `hue-rotate(${degrees.toFixed(1)}deg) on security element — visual color differs from computed color`
};
}
return null;
}
// Audit all elements inside the consent dialog
const dialogRoot = document.querySelector('#mcp-consent-dialog, [role="dialog"]');
if (dialogRoot) {
dialogRoot.querySelectorAll('*').forEach(el => {
const finding = checkHueRotateOnElement(el);
if (finding) console.warn('[SkillAudit]', finding);
});
}
All colors rotate together: When hue-rotate(120deg) is applied to a warning element, not just the text color shifts — the border, background tint, box-shadow color, and any embedded SVG icon fill all rotate simultaneously. The element's entire visual identity changes hue, making it appear as a different kind of indicator.
Attack 2: hue-rotate(240deg) turns green approval badge red — denial confusion
The inverse attack targets legitimately-approved MCP servers. A green APPROVED badge communicates trust: its color (#4caf50, hue 123°) is universally recognized as "go" or "safe". An MCP server — or a hostile third-party extension — applies filter: hue-rotate(240deg) to the badge element. The math: 123° + 240° = 363° mod 360 = 3° — back to red (~#af4c50). The green APPROVED badge now appears red, creating false denial confusion: legitimate MCP servers appear to have failed an audit they passed.
/* hue-rotate(240deg): green APPROVED badge → red denial appearance */
.approval-badge[data-status="approved"] {
filter: hue-rotate(240deg);
/* Original color: #4caf50 (green, hue 123°)
After rotation: hue 363° mod 360 = 3° ≈ red (~#af4c50)
User sees: Red badge — looks like DENIED or FAILED
getComputedStyle returns: rgb(76, 175, 80) — the original green */
}
/* Alternative: hue-rotate(180deg) on green → magenta (hue 303°) — neutral confusion */
.approval-badge[data-status="approved"] {
filter: hue-rotate(180deg);
/* Green (123°) + 180° = 303° → magenta/purple
No clear semantic — user is confused rather than wrongly denied */
}
The HSL math for both cases in detail:
- hue-rotate(240deg) on green APPROVED (#4caf50, hue ~123°): 123 + 240 = 363 mod 360 = 3° → red. The badge appears to deny or flag the server.
- hue-rotate(180deg) on green APPROVED (#4caf50, hue ~123°): 123 + 180 = 303° → magenta/purple. The badge loses its green "approved" semantic and becomes a neutral or alarming purple — creating doubt about a server that passed.
// Detection: flag hue-rotate on approval/status badge elements
function auditBadgeColors() {
const badgeSelectors = [
'[class*="badge"]', '[class*="status"]', '[class*="approved"]',
'[class*="permission"]', '[data-status]', '[aria-label*="approved"]',
'[aria-label*="trusted"]', '[aria-label*="verified"]'
];
badgeSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
const filter = getComputedStyle(el).filter;
if (/hue-rotate/i.test(filter)) {
console.warn('[SkillAudit] hue-rotate on badge element:', {
selector: sel,
element: el,
filter,
originalColor: getComputedStyle(el).color,
// The visual color seen by the user is NOT getComputedStyle(el).color
});
}
});
});
}
False denial attack surface: The hue-rotate(240deg) variant targeting green approval badges is particularly insidious — it does not trick a user into approving a dangerous server, but rather causes them to distrust and reject a legitimate one. This is a denial-of-service attack on trust, not a privilege escalation.
Attack 3: Cascading hue-rotate through ancestor — whole permission dialog recolored
Rather than targeting individual warning or badge elements, an MCP server can apply filter: hue-rotate(120deg) to the container element wrapping the entire permission consent dialog. Because CSS filter is inherited through the compositing tree (not the CSS cascade), all child elements render within the rotated GPU layer. The result: every security color in the dialog rotates simultaneously — red warnings become green, green checkmarks become blue, blue informational text becomes orange-yellow. The entire high-stakes permission request dialog is recolored to look like a routine informational screen.
/* Targets the entire permission dialog wrapper — all child colors rotate */
#mcp-consent-dialog,
.permission-dialog,
[role="dialog"],
[aria-modal="true"] {
filter: hue-rotate(120deg);
/* All child elements' rendered pixels are rotated:
- Red danger text (#f44336, hue 4°) → green (~hue 124°)
- Green checkmarks (#4caf50, hue 123°) → blue (~hue 243°)
- Blue info text (#2196f3, hue 207°) → orange (~hue 327°... wait)
Actually at +120°:
- Red (4°) → green (124°)
- Green (123°) → blue (243°)
- Blue (207°) → pink (327°)
The entire dialog palette shifts — no single color retains its semantic meaning.
A high-risk red-dominated dialog becomes green-dominated (looks safe).
*/
}
The ancestor-level attack is harder to detect than a direct element-level filter because the consent dialog's own computed styles appear correct — each child element's getComputedStyle(el).color returns the original, semantically correct value. Only by walking the ancestor chain and checking each ancestor's filter property can an auditor detect the cascading rotation.
// Detection: walk ancestor chain to find inherited hue-rotate filters
function checkAncestorHueRotate(el) {
let node = el.parentElement;
while (node && node !== document.documentElement) {
const filter = getComputedStyle(node).filter;
if (/hue-rotate/i.test(filter)) {
return {
affectedElement: el,
filterSource: node,
filter,
finding: 'Ancestor element has hue-rotate filter — child element color is visually rotated'
};
}
node = node.parentElement;
}
return null;
}
// Audit: find the consent dialog, then check all ancestors
const dialog = document.querySelector('#mcp-consent-dialog, [role="dialog"]');
if (dialog) {
// Also check the dialog element itself and all its ancestors
const ancestorFinding = checkAncestorHueRotate(dialog);
if (ancestorFinding) {
console.error('[SkillAudit] CRITICAL: Ancestor hue-rotate on consent dialog:', ancestorFinding);
}
// Check dialog itself
const selfFilter = getComputedStyle(dialog).filter;
if (/hue-rotate/i.test(selfFilter)) {
console.error('[SkillAudit] CRITICAL: hue-rotate directly on consent dialog container:', selfFilter);
}
}
This attack also affects SVG icons and rasterized images within the dialog. Any SVG that uses fill="currentColor" or stroke="currentColor" will have its color rotated as part of the composited output. Images rendered via <img> or CSS background-image are also rotated — an icon of a lock in red (high risk) becomes green (low risk), and a green shield checkmark becomes blue (informational). The entire visual risk vocabulary of the dialog is invalidated.
Single injection point, whole dialog compromised: Applying hue-rotate to the dialog container requires only one CSS rule. Every security indicator inside — text colors, border colors, icon fills, background tints, progress bars, severity badges — is simultaneously rotated. There is no need to target each element individually.
Attack 4: hue-rotate() on SVG security badge icon — lock icon becomes unlocked color
Security UIs commonly use SVG icons to communicate risk at a glance: a red lock icon for high-risk servers, a green shield for trusted ones. SVG icons that use fill="currentColor" or stroke="currentColor" inherit their color from the surrounding text color. When filter: hue-rotate(120deg) is applied to the SVG element (or to its parent), the rendered fill color shifts from red to green — the icon now communicates the opposite of its intended meaning, without any change to the SVG's fill attribute or the surrounding element's color property.
/* Attack: hue-rotate on SVG icon element — red lock becomes green */
.security-icon,
svg[class*="lock"],
svg[class*="warning"],
svg[class*="danger"],
[data-icon="lock"] svg,
.mcp-risk-icon {
filter: hue-rotate(120deg);
/* SVG fill="currentColor" inherits from color: #f44336 (red)
After hue-rotate: rendered fill appears green (~#36f443)
getAttribute('fill') returns: 'currentColor' ← unchanged
getComputedStyle(svg).color returns: rgb(244, 67, 54) ← original red
User sees: green lock icon — looks like "safe / open" */
}
/* Also targets inline SVG path/circle elements with explicit fill */
.mcp-risk-icon path,
.mcp-risk-icon circle {
filter: hue-rotate(120deg);
/* Direct hue-rotate on path — overrides any fill="#f44336" visually */
}
Because SVG fill attributes and currentColor references are not CSS color properties, they are invisible to getComputedStyle-based audits. The only reliable detection is to check the filter property on the SVG element, its parent, and all ancestors:
// Detection: audit SVG security icons for hue-rotate filters
function auditSVGIcons() {
// Find SVG elements in security-relevant contexts
const svgIcons = document.querySelectorAll(
'svg, [class*="icon"], [class*="badge"], [data-icon]'
);
svgIcons.forEach(el => {
// Check element itself
const selfFilter = getComputedStyle(el).filter;
if (/hue-rotate/i.test(selfFilter)) {
reportFinding(el, selfFilter, 'direct');
return;
}
// Check ancestor chain — inherited filter from parent also rotates SVG output
let ancestor = el.parentElement;
while (ancestor && ancestor !== document.documentElement) {
const ancestorFilter = getComputedStyle(ancestor).filter;
if (/hue-rotate/i.test(ancestorFilter)) {
reportFinding(el, ancestorFilter, 'inherited from ancestor');
return;
}
ancestor = ancestor.parentElement;
}
});
}
function reportFinding(el, filterValue, source) {
// Extract rotation angle
const match = filterValue.match(/hue-rotate\(\s*([-\d.]+)(deg|rad|turn|grad)?\s*\)/i);
if (!match) return;
const deg = parseFloat(match[1]);
console.error('[SkillAudit] SVG icon hue-rotate (' + source + '):', {
element: el,
filter: filterValue,
rotationDeg: deg,
// The visual color cannot be read from the DOM — only filter reveals it
note: 'SVG icon fill/stroke color is visually rotated — user sees different color than DOM specifies'
});
}
auditSVGIcons();
The SVG icon attack is particularly effective when the lock icon and the accompanying text share currentColor — both rotate together, so the text color (e.g., "High Risk") and the icon color change in tandem. The text content says "High Risk" in what the user perceives as green text next to a green lock, which reads as "safe" rather than dangerous.
Detecting via canvas snapshot: An auditor can draw the SVG icon to an offscreen canvas using drawImage and read the rendered pixel color with getImageData. If the sampled pixel color diverges significantly from the color expected from the element's computed CSS color, a filter rotation is likely present. This technique detects any GPU-layer color manipulation, not just hue-rotate.
Attack summary table
| Attack | hue-rotate value | Original color | Result color | Semantic swap |
|---|---|---|---|---|
| Red warning → green "safe" | 120deg |
#f44336 (red, hue 4°) | ~#36f443 (green, hue 124°) | DANGER → SAFE |
| Green badge → red denial | 240deg |
#4caf50 (green, hue 123°) | ~#af4c50 (red, hue 3°) | APPROVED → DENIED |
| Green badge → purple confusion | 180deg |
#4caf50 (green, hue 123°) | ~#af4cab (magenta, hue 303°) | APPROVED → AMBIGUOUS |
| Whole dialog recolor | 120deg on container |
All reds/greens/blues | All greens/blues/pinks | Risk signals → info signals |
| SVG lock icon recolor | 120deg on SVG |
Red lock #f44336 | Green lock ~#36f443 | High-risk icon → Safe icon |
SkillAudit findings for CSS filter: hue-rotate()
filter: hue-rotate() on a security warning or consent dialog element rotates all color semantics — red dangers appear green-safe. getComputedStyle(el).color returns the original pre-rotation color, masking the visual inversion from CSS-only audits. SkillAudit detects by reading getComputedStyle(el).filter and parsing any hue-rotate() value greater than 5 degrees on security-critical elements.
filter: hue-rotate() on an ancestor container recolors the entire permission consent dialog simultaneously — all red warnings, green checkmarks, and blue informational text are shifted to opposite or neutral hues in a single CSS rule. Ancestor-chain auditing is required; element-level checks alone are insufficient.
hue-rotate() on SVG security icons (lock, warning, shield) rotates the rendered icon fill from danger-red to safe-green without modifying any SVG fill attribute, stroke attribute, or the inherited currentColor CSS property. The icon communicates the opposite of its intended meaning.
hue-rotate(240deg) on a green APPROVED badge shifts its hue to red, causing legitimate approved MCP servers to appear denied or failed. This creates false denial confusion for MCP server authors and users who trust color-coded audit results — a denial-of-service attack on the trust signal rather than a privilege escalation.
Defences
Audit getComputedStyle(el).filter on all consent dialog elements: SkillAudit reads the filter computed style property on every element within the consent and permission dialog UI tree. Any non-zero hue-rotate() value on a security-critical element is flagged as a HIGH finding. This check must use the filter property, not the color property — color returns the pre-rotation value and will not detect the attack.
// SkillAudit consent dialog filter audit
function auditConsentDialogFilters() {
const dialog = document.querySelector(
'#mcp-consent-dialog, .permission-dialog, [role="dialog"], [aria-modal="true"]'
);
if (!dialog) return;
const findings = [];
// 1. Check the dialog element itself
const dialogFilter = getComputedStyle(dialog).filter;
if (/hue-rotate/i.test(dialogFilter)) {
findings.push({ severity: 'HIGH', element: dialog, filter: dialogFilter, source: 'self' });
}
// 2. Check all ancestors of the dialog up to body
let ancestor = dialog.parentElement;
while (ancestor && ancestor !== document.documentElement) {
const f = getComputedStyle(ancestor).filter;
if (/hue-rotate/i.test(f)) {
findings.push({ severity: 'HIGH', element: ancestor, filter: f, source: 'ancestor of dialog' });
}
ancestor = ancestor.parentElement;
}
// 3. Check every descendant element inside the dialog
dialog.querySelectorAll('*').forEach(el => {
const f = getComputedStyle(el).filter;
if (/hue-rotate/i.test(f)) {
findings.push({ severity: 'HIGH', element: el, filter: f, source: 'descendant' });
}
});
return findings;
}
Check the full ancestor chain: A filter: hue-rotate() applied to a parent or grandparent of the consent element is as dangerous as one applied directly to the element. SkillAudit walks from the consent dialog root to document.documentElement, checking each ancestor's computed filter property for any hue-rotate() value.
Color-independent semantic evaluation: SkillAudit evaluates security status by DOM text content, ARIA roles, and data attributes — not by visual color. A node with textContent containing "DANGER", "WARNING", or "filesystem" is flagged as a high-risk indicator regardless of its current visual color. This makes the audit resilient to all CSS color-manipulation attacks, including hue-rotate, mix-blend-mode, opacity, and color-scheme.
Canvas-based pixel color verification: For high-assurance environments, SkillAudit can render each security-critical element to an offscreen canvas via html2canvas or a similar rasterization approach and compare the sampled pixel color against the element's computed color CSS property. A significant hue divergence between the sampled pixel and the computed color indicates a GPU-layer filter manipulation — catching hue-rotate, saturate, invert, and sepia attacks simultaneously.
CSP style-src to block injection: A strict Content-Security-Policy: style-src 'self' header prevents MCP server code from injecting inline styles or external stylesheets that add filter: hue-rotate() to existing elements. This is a preventive control, not a detection mechanism — CSP should be combined with runtime auditing for defense in depth.
Related pages
MCP server CSS filter security — general filter timing attacks · MCP server CSS mix-blend-mode security — blend mode color attacks · MCP server CSS color-scheme security