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:

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:

/* 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()

CRITICAL 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.
HIGH 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.
CRITICAL Values in the range 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.
MEDIUM Any 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