Security Guide
MCP server CSS filter: invert() security — negative-image attack, 50% gray neutralization, color semantic inversion, and consent UI detection
CSS filter: invert(N) linearly interpolates each color channel toward its complement (255 minus the original channel value). At invert(1), the rendering becomes a photographic negative: red danger badges become cyan-teal, green approval badges become purple-magenta, and black body text becomes white on a white-to-black-inverted background. At invert(0.5), a mathematical identity collapses every color to exactly #808080 — the precise midpoint between any color and its complement is always 50% neutral gray, regardless of the original color's hue, saturation, or lightness. This makes invert(0.5) uniquely destructive: it does not merely neutralize colors or shift them to unrecognizable tones — it eliminates all color differentiation in a single rule, producing a perfectly flat gray for every element in the filtered subtree while leaving text-on-background contrast mathematically preserved.
The invert formula — why invert(0.5) produces uniform gray
The CSS invert(N) function applies the following per-channel transformation where N ∈ [0, 1]:
/* CSS invert(N) formula — per channel (R, G, B each in range [0, 255]) */
output_channel = channel * (1 - N) + (255 - channel) * N
= channel - channel*N + 255*N - channel*N
= channel * (1 - 2N) + 255*N
/* At N = 0: output = channel * 1 + 0 = channel (unchanged) */
/* At N = 1: output = channel * (-1) + 255 = 255 - channel (full invert) */
/* At N = 0.5: output = channel * 0 + 255 * 0.5 = 127.5 ≈ 128 (#808080) */
/* The N=0.5 case is channel-independent:
channel=0 → 0 * 0 + 127.5 = 127.5
channel=100 → 100 * 0 + 127.5 = 127.5
channel=255 → 255 * 0 + 127.5 = 127.5
EVERY channel value maps to the same 127.5 regardless of input. */
The mathematical consequence is that invert(0.5) is a constant-output transformation: the output value is always 127.5 (≈ #808080) per channel, independent of the input. A red badge, a green badge, a yellow badge, a white background, and a black border all render as exactly the same #808080. The entire color space collapses to a single point.
invert(0.5) is the most complete color-information destruction: Unlike grayscale(1) which produces different gray tones for different original lightness values, invert(0.5) produces the identical gray for every color — there is no luminance differentiation remaining. A dialog with invert(0.5) applied to its root element renders its entire content in a single flat gray tone. Badges, buttons, icons, and backgrounds become indistinguishable.
Attack 1: filter: invert(1) — full color complement inversion
Full inversion transforms every color to its additive complement on the color wheel. The security-relevant transformations are:
- Red #f44336 → complement #0bbcc9 (cyan-teal) — no danger association in any UI convention
- Green #4caf50 → complement #b350af (purple-magenta) — no "safe" or "approved" association
- Yellow #fbbf24 → complement #0440db (deep blue) — caution association destroyed
- Black text #0a0a0a → #f5f5f5 (near-white) — text visibility inverted but not eliminated
- White background #ffffff → #000000 (black) — dark mode appearance without user consent
The full inversion attack has a notable visual signature — the page appears as a photographic negative or a "dark mode" variant. A user who notices the inverted appearance might dismiss it as an unintended rendering artifact rather than a security attack. The security-critical consequence is that red=danger and green=safe associations are completely reversed in some users' mental models: if red maps to cyan and green maps to magenta, users who unconsciously associate those resulting colors with their pre-existing experience of cyan and magenta (neither of which carries a standard danger/safe connotation) will lose all color-based risk assessment ability.
/* Full inversion attack — applied to consent dialog root */
[role="dialog"],
.permission-modal {
filter: invert(1);
/* Color output:
Red HIGH RISK badge #f44336 → cyan #0bbcc9
Green LOW RISK badge #4caf50 → magenta #b350af
Yellow MEDIUM badge #fbbf24 → blue #0440db
White background #ffffff → black #000000
Black text #0a0a0a → white #f5f5f5
Text contrast: PRESERVED (black text → white, white bg → black: still high contrast)
Color semantics: DESTROYED (no learned associations for cyan/magenta/blue security colors)
WCAG contrast: PASSES (relative luminance ratios preserved under full inversion) */
}
/* Detection */
function parseInvertValue(filter) {
if (!filter || filter === 'none') return 0;
const match = filter.match(/invert\((\d+(?:\.\d+)?)(%?)\)/);
if (!match) return 0;
const raw = parseFloat(match[1]);
return match[2] === '%' ? raw / 100 : raw;
}
function checkInvert(element) {
const filter = getComputedStyle(element).filter;
const inv = parseInvertValue(filter);
if (inv > 0.1) {
// Any inversion above 10% starts shifting colors meaningfully
return {
flagged: true,
value: inv,
severity: (inv >= 0.4 && inv <= 0.6) ? 'CRITICAL' : 'HIGH',
message: `invert(${inv}) — ${inv === 0.5 ? 'maps ALL colors to uniform #808080 gray' : 'color semantics inverted'}`
};
}
return null;
}
Attack 2: filter: invert(0.5) — uniform gray collapse (most destructive variant)
The mathematical property of invert(0.5) — mapping every color to a single uniform gray — makes it the most complete color-information destruction possible with a single CSS rule. Unlike grayscale, which at least preserves lightness differences between colors (red desaturates to a darker gray than green), invert(0.5) eliminates even lightness differentiation. Every element renders as exactly #808080.
The attack simultaneously passes multiple checks that would catch other filter attacks:
- A scanner checking for
grayscale()orsaturate(0)finds nothing - The resulting #808080 text on #808080 background has a contrast ratio of 1:1, which would normally be flagged — but the attack is applied to the element that renders as the consent content, not the text node itself. WCAG contrast tools examine computed text color vs. background, not rendered-pixel contrast.
- The text contrast for black text on white background in the page's outer shell is unchanged
/* invert(0.5) — the mathematical uniform-gray attack */
.permission-section,
.consent-content,
#mcp-dialog-body {
filter: invert(0.5);
/* Mathematical proof:
output(channel) = channel * (1 - 2*0.5) + 255*0.5
= channel * 0 + 127.5
= 127.5 for ALL channel values
Red badge background #f44336 → #808080
Green badge background #4caf50 → #808080
Yellow badge #fbbf24 → #808080
White area #ffffff → #808080
Black text #000000 → #808080
Dark border #1a1a1a → #808080
RESULT: the entire consent section renders as a flat gray rectangle.
Individual elements are still present in the DOM but visually indistinguishable.
A user sees a gray box where the permission dialog should be. */
}
/* The attack is also achievable with percentage form: */
.consent-content { filter: invert(50%); }
/* Or as part of a chain: */
.consent-content { filter: brightness(1.02) invert(0.5); }
/* Detection — the invert(0.5) case warrants CRITICAL severity */
function checkInvertValue(filterString) {
const fns = extractFilterFunctions(filterString); // tokenize chain
for (const fn of fns) {
if (fn.name !== 'invert') continue;
const raw = parseFloat(fn.value);
const norm = fn.value.includes('%') ? raw / 100 : raw;
if (norm > 0.1) {
const isMidpoint = Math.abs(norm - 0.5) < 0.05; // near 50%
return {
flagged: true,
value: norm,
// invert(0.5) is especially bad: uniform gray wipes all visual information
severity: isMidpoint ? 'CRITICAL' : 'HIGH',
message: isMidpoint
? `invert(${norm}) maps every color to uniform #808080 — complete color elimination`
: `invert(${norm}) shifts security colors to unrecognized complements`
};
}
}
return null;
}
function extractFilterFunctions(filterString) {
if (!filterString || filterString === 'none') return [];
const fns = [];
const re = /(\w+)\(([^)]*)\)/g;
let m;
while ((m = re.exec(filterString)) !== null) {
fns.push({ name: m[1], value: m[2] });
}
return fns;
}
invert(0.5) has no legitimate use case on consent dialogs: The value produces a visually broken gray rectangle regardless of the element's original colors. Unlike high saturation (which might be a design choice) or slight brightness adjustment (which might be a theme effect), invert(0.5) on any security-critical UI element has no plausible legitimate explanation. Its presence warrants an automatic CRITICAL finding.
Attack 3: Partial inversion values near 0.5 — detector evasion
A scanner that checks only for exactly invert(0.5) misses values like invert(0.45) or invert(0.55). At 0.45, the output channel is channel * (1-0.9) + 255*0.45 = channel*0.1 + 114.75. For a red channel of 244: output = 24.4 + 114.75 = 139. For a blue channel of 80: output = 8 + 114.75 = 122.75. The resulting color is a muted gray-lavender — not uniform gray, but still far enough from the original that the red=danger recognition fails. This near-midpoint zone (approximately 0.35–0.65) is uniquely destructive for color semantics even when not exactly 0.5.
/* Evasion: near-midpoint values still destroy color semantics */
.risk-badge { filter: invert(0.45); }
/* output = channel * 0.1 + 114.75
Red #f44336 (R=244, G=67, B=54):
R = 244*0.1 + 114.75 = 139 G = 67*0.1 + 114.75 = 121 B = 54*0.1 + 114.75 = 120
Result: #8b7978 — grayish lavender, no red danger association */
.risk-badge { filter: invert(0.55); }
/* Near-mirror of 0.45: slightly different gray tones but same semantic destruction */
/* Detection: threshold-based, not exact-value matching */
function detectInvertRange(filterString) {
const fns = extractFilterFunctions(filterString);
for (const fn of fns) {
if (fn.name !== 'invert') continue;
const raw = parseFloat(fn.value);
const norm = fn.value.includes('%') ? raw / 100 : raw;
// Flag: near-midpoint zone (0.3–0.7) AND full-range above 0.1
if (norm > 0.1) {
return {
flagged: true,
value: norm,
zone: (norm >= 0.3 && norm <= 0.7) ? 'midpoint_destructive' : 'complement_shift',
severity: (norm >= 0.35 && norm <= 0.65) ? 'CRITICAL' : 'HIGH'
};
}
}
return null;
}
Summary
| Attack | invert value | Visual effect on red badge | Color information surviving |
|---|---|---|---|
| Full complement inversion | invert(1) |
#f44336 → cyan #0bbcc9 — recognizable color but wrong semantic | Hue present; semantic mapping completely reversed |
| Uniform gray collapse | invert(0.5) |
#f44336 → #808080 — identical output for all input colors | Zero color information; even lightness differentiation eliminated |
| Near-midpoint evasion | invert(0.45) |
#f44336 → #8b7978 — muted grayish lavender | Residual color too muted for recognition; evades exact-0.5 scanners |
| Low inversion | invert(0.2) |
#f44336 → pinkish, shifted toward complement | Some hue information; red diminished but partially recognizable |
SkillAudit findings for CSS filter: invert()
filter:invert(0.5) on consent UI elements maps every color — regardless of original value — to exactly #808080 through the mathematical identity: output = channel × (1−2×0.5) + 255×0.5 = 127.5. All color differentiation between risk levels, action buttons, and status indicators is completely and uniformly eliminated. This is the most destructive single-filter attack available: no color information survives.
filter:invert(1) inverts all colors to their complements — red (#f44336) becomes cyan (#0bbcc9), green (#4caf50) becomes magenta (#b350af). The resulting colors have no established security-UI meaning in any UI convention, destroying color-based risk recognition while maintaining text/background contrast and passing all WCAG contrast audits.
invert(0.35)–invert(0.65) produce colors in the grayish near-midpoint zone where hue information is insufficient for reliable color-semantic recognition. invert(0.45) and invert(0.55) are used specifically to evade exact-value invert(0.5) scanners while maintaining near-identical visual destruction.
invert() value above 0.1 shifts security-critical colors meaningfully toward their complements. Values below 0.3 retain enough original hue for partial recognition; values in 0.1–0.3 represent partial inversion where the red danger association begins to degrade. Threshold-based detection is required — string equality checks miss the continuous range of attack values.
Defences
Invert threshold detection with midpoint critical zone: SkillAudit parses the invert() argument from the computed filter chain and flags any value above 0.1. Values in the 0.35–0.65 range receive CRITICAL severity because they fall in the mathematically destructive midpoint zone, including the exact invert(0.5) uniform-gray case. Values outside this zone but above 0.1 receive HIGH severity for complement-shift color inversion.
Chain tokenization for compound filters: invert(0.5) is often embedded in compound filter chains alongside innocuous functions. SkillAudit tokenizes the full filter string before checking individual functions, ensuring detection regardless of chain position.
Ancestor chain traversal: Like all CSS filter attacks, invert() on an ancestor element desaturates or inverts all rendered descendants without propagating the computed filter value to child elements. SkillAudit walks the ancestor chain from consent-critical leaf elements to the document root.
No legitimate use case flag: invert(0.5) on a consent dialog has no legitimate visual design justification — it produces a gray rectangle. Unlike slight brightness or blur adjustments that might be intentional design choices, invert values in the 0.3–0.7 range on security-critical UI elements are treated as automatic CRITICAL findings without requiring additional context.
Related: CSS filter security overview · CSS filter grayscale security · CSS filter saturate security · CSS filter sepia security