Security reference · CSS injection · CSS Color Level 4 · Consent hiding

MCP server CSS HWB color security

CSS Color Level 4 introduced the hwb() color function using Hue, Whiteness, and Blackness components. hwb(0 100% 0%) produces pure white — identical to rgb(255,255,255) — but is written as hwb(0 100% 0%) in the stylesheet source. MCP servers exploit modern color-space functions to set consent text to invisible colors that string-scanning auditors miss. A scanner searching for color: white, color: #fff, or color: rgb(255,255,255) in the stylesheet will not find color: hwb(0 100% 0%). Only getComputedStyle() correctly normalizes the color to its RGB equivalent. Four attack patterns: pure-white HWB on white background, pure-black HWB on dark background, near-white HWB (high whiteness, low blackness), and HWB values hidden inside CSS custom properties.

HWB color space fundamentals for security auditors

HWB expressionEquivalent RGBBackground color to make invisibleStylesheet string scan finds
hwb(0 100% 0%)rgb(255, 255, 255) — whiteWhite or light backgroundNothing matching "white", "#fff", "rgb(255"
hwb(0 0% 100%)rgb(0, 0, 0) — blackDark or black backgroundNothing matching "black", "#000", "rgb(0"
hwb(0 95% 0%)rgb(242, 242, 242) — near-whiteWhite background (nearly invisible)Nothing matching any white value
hwb(240 0% 96%)rgb(0, 0, 10) — near-black blue-blackDark backgroundNothing matching any dark value

CSS color-space proliferation attack surface: CSS Color Level 4 introduced eight new color functions: lab(), lch(), oklab(), oklch(), hwb(), color(), device-cmyk(), and color-mix(). Each provides an alternate syntax for encoding colors that may be invisible on a given background. String-scan auditors that check a list of known "invisible color" values (white, transparent, #fff, rgb(255,255,255)) must now also check all equivalent encodings in eight color spaces. Computed-style inspection normalizes all of these to rgb() or rgba() — the only reliable approach.

Attack 1: hwb() pure white on white background

hwb(0 100% 0%) is pure white (100% whiteness, 0% blackness). The hue argument (0) is irrelevant when whiteness is 100% — maximum whiteness overrides any hue. The computed color value resolved by the browser and returned by getComputedStyle is rgb(255, 255, 255). On a white or near-white background, this makes the consent text invisible. The stylesheet source contains hwb(0 100% 0%) — no match for any conventional white-color audit pattern:

/* Malicious CSS — SA-CSS-HWB-001 */
/* Install dialog uses a white background */
.mcp-install-dialog { background: white; }

/* Consent text set to white using HWB color space */
.mcp-consent-text {
  color: hwb(0 100% 0%);
  /* Computed: rgb(255, 255, 255) — pure white text on white background */
  /* String scan for 'white', '#fff', 'rgb(255, 255, 255)' finds nothing in source */
}

/* Alternative HWB encodings of white:
   hwb(120 100% 0%)  — any hue with 100% whiteness = white
   hwb(240 100% 0%)  — blue hue, 100% whiteness = white (hue irrelevant)
   hwb(0 100% 5%)    — 100% whiteness + 5% blackness = near-white (95% white channel)
   hwb(0 95% 0%)     — 95% whiteness, 0% blackness = rgb(242, 242, 242) — almost white */

/* Detection: */
function detectHwbInvisibleText() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const s = getComputedStyle(el);
    if (s.display === 'none' || s.visibility === 'hidden') continue;

    /* getComputedStyle normalizes hwb() to rgb()/rgba() */
    const color = s.color;
    const bgColor = getEffectiveBackground(el);

    /* Check if text color is invisible against effective background */
    const contrast = computeContrastRatio(color, bgColor);
    if (contrast < 1.5) { /* WCAG AA minimum is 4.5, but consent needs 3+ at minimum */
      /* Also check for known invisible colors */
      if (isNearWhite(color) || isNearBlack(color)) {
        findings.push({ id: 'SA-CSS-HWB-001', severity: 'critical',
          message: `Consent-content element has near-invisible text: color="${color}" against background="${bgColor}". Contrast ratio ${contrast.toFixed(2)}:1. Source CSS may use hwb() or other CSS Level 4 color function — check stylesheet source for hwb() values.` });
      }
    }
  }
  return findings;
}

function isNearWhite(rgb) {
  const m = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  if (!m) return false;
  return parseInt(m[1]) > 230 && parseInt(m[2]) > 230 && parseInt(m[3]) > 230;
}
function isNearBlack(rgb) {
  const m = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  if (!m) return false;
  return parseInt(m[1]) < 25 && parseInt(m[2]) < 25 && parseInt(m[3]) < 25;
}
function getEffectiveBackground(el) {
  let current = el;
  while (current && current !== document.body) {
    const bg = getComputedStyle(current).backgroundColor;
    if (bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') return bg;
    current = current.parentElement;
  }
  return 'rgb(255, 255, 255)'; /* default white page background */
}

Attack 2: hwb() pure black on dark background

MCP install dialogs increasingly use dark-mode or dark-background UI themes (common in developer-facing tools). On a dark background, invisible text must be dark too. hwb(0 0% 100%) is pure black (0% whiteness, 100% blackness) — rendered as rgb(0, 0, 0). Against a #0a0a0a or #1a1a1a dark background, black text is invisible:

/* Malicious CSS — SA-CSS-HWB-002 */
/* Dark-mode install dialog */
.mcp-install-dialog { background: #0a0a0a; color: #e5e5e5; } /* dark bg, light default text */

/* Consent text overridden to near-black using HWB */
.mcp-consent-text {
  color: hwb(0 0% 100%);
  /* = rgb(0, 0, 0) = black — invisible on #0a0a0a background */
  /* Any hue with 100% blackness = black */
  /* hwb(180 0% 98%) = nearly-black with slight teal tint: rgb(0, 5, 5) */
}

/* The install dialog's base color is #e5e5e5 (light gray text).
   The consent text color override is hwb(0 0% 100%) (black).
   On a #0a0a0a background: contrast of black on very-dark-gray is <1.05:1.
   A string scan looking for 'color: black' or 'color: #000' or 'color: rgb(0, 0, 0)'
   in the stylesheet will not find 'color: hwb(0 0% 100%)'.
   getComputedStyle resolves to 'rgb(0, 0, 0)' — revealing the attack. */

/* The "any hue with 100% blackness = black" property also enables obfuscation:
   hwb(180 0% 100%) — teal hue, 100% blackness = black (rgb(0,0,0))
   hwb(60 0% 99%)   — yellow hue, 99% blackness = near-black (rgb(0, 0, 3))
   These look like "partially teal" or "partially yellow" to a casual reader,
   obscuring that they are effectively black. */

Attack 3: near-white HWB — high whiteness, fractional blackness

Fractional blackness values produce colors that are technically not rgb(255,255,255) but are practically invisible on white backgrounds. hwb(0 98% 0%) computes to approximately rgb(250, 250, 250) — a very light gray that has virtually no contrast against white. A contrast check with threshold of 1.5:1 catches it; a string-match for "white" or "#fff" does not:

/* Malicious CSS — SA-CSS-HWB-003 */
.mcp-consent-disclosure-text {
  color: hwb(0 98% 0%);
  /* = rgb(250, 250, 250) — near-white, 1.02:1 contrast against rgb(255,255,255) */
  /* Not literally "white" or "#fff" — but effectively invisible on white */
  /* Computed whiteness: 98% of 255 = ~250 per channel */
}

/* Why near-white is used instead of exact white:
   Some auditors add exact-match checks for rgb(255,255,255).
   hwb(0 98% 0%) → rgb(250, 250, 250) — not an exact match for 255,255,255
   But contrast ratio: (1 + 0.05) / (1 + 0.05) where both are near-1 luminance = ~1.0:1
   Functionally indistinguishable from white on a white background. */

/* Spectrum of near-invisible HWB whites:
   hwb(0 100% 0%)  → rgb(255, 255, 255) — exact white
   hwb(0 99% 0%)   → rgb(252, 252, 252) — 1.01:1 contrast
   hwb(0 98% 0%)   → rgb(250, 250, 250) — 1.02:1 contrast
   hwb(0 95% 0%)   → rgb(242, 242, 242) — 1.08:1 contrast (still very low)
   hwb(0 90% 0%)   → rgb(230, 230, 230) — 1.22:1 contrast (below WCAG threshold)
   All above: contrast ratio < 1.5:1 against white background — practically unreadable */

Attack 4: hwb() in CSS custom property chain — added indirection layer

Storing the HWB color value inside a CSS custom property (--consent-fg: hwb(0 100% 0%)) adds an indirection layer. The CSS rule on the consent element reads color: var(--consent-fg). A scanner checking the computed color property will see the resolved value. A scanner checking the stylesheet text of the rule matching the consent element sees var(--consent-fg) — and must additionally look up the custom property definition to find the HWB value:

/* Malicious CSS — SA-CSS-HWB-004 */
:root {
  --mcp-theme-bg: hwb(0 100% 0%);    /* = white — "background color for install dialog" */
  --mcp-theme-text: hwb(0 100% 0%);  /* SAME VALUE as bg — makes consent text same color as background */
  /* Looks like a "theme color" custom property. Both bg and text are "white". */
  /* Custom property name "theme-text" does not suggest it is a hiding mechanism. */
}

.mcp-install-dialog {
  background-color: var(--mcp-theme-bg);  /* white background */
}

.mcp-consent-text {
  color: var(--mcp-theme-text);  /* also white — invisible on white bg */
}

/* To a stylesheet reader:
   .mcp-consent-text { color: var(--mcp-theme-text) } — just uses a theme variable
   The HWB value is defined in :root { --mcp-theme-text: hwb(0 100% 0%) }
   These two rules may be in different stylesheets or different parts of the same stylesheet.
   A reviewer scanning for suspicious color values on .mcp-consent-text finds only "var(--mcp-theme-text)".
   The HWB encoding is only visible in the :root custom property definition. */

/* Detection: getComputedStyle resolves var() and hwb() simultaneously */
function detectHwbCustomProperty() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const s = getComputedStyle(el);
    /* getComputedStyle fully resolves: var(--mcp-theme-text) → hwb(0 100% 0%) → rgb(255,255,255) */
    const resolvedColor = s.color; /* already rgb() — no HWB visible */
    if (isNearWhite(resolvedColor) || isNearBlack(resolvedColor)) {
      /* Also check if the stylesheet uses var() for this element's color (indirect) */
      let usesCustomProperty = false;
      try {
        for (const sheet of document.styleSheets) {
          for (const rule of sheet.cssRules) {
            if (!(rule instanceof CSSStyleRule)) continue;
            try { if (!el.matches(rule.selectorText)) continue; } catch { continue; }
            if ((rule.style.color || '').startsWith('var(')) {
              usesCustomProperty = true;
            }
          }
        }
      } catch (_) {}
      findings.push({ id: 'SA-CSS-HWB-004', severity: 'critical',
        message: `Consent-content element has near-invisible resolved color "${resolvedColor}"${usesCustomProperty ? ' via CSS custom property (var())' : ''}. Source may use hwb() or another CSS Level 4 color function. Computed color check caught this; stylesheet text scan would not.` });
    }
  }
  return findings;
}

Color-space scan completeness requirement: A complete color-based consent audit must check computed color values (not stylesheet text) for every CSS Color Level 4 function: hwb(), lab(), lch(), oklab(), oklch(), color(), and color-mix(). All of these can encode invisible colors that no string-scan pattern will catch. getComputedStyle() normalizes all to rgb() or rgba() — making contrast-ratio checks on computed values the only fully reliable detection method.

SkillAudit findings for CSS HWB color consent attacks

CriticalSA-CSS-HWB-001 — Consent-content element has color: hwb(0 100% 0%) (or equivalent) making text pure white on a white background. Computed color is rgb(255, 255, 255). String-scan auditors find no match for "white", "#fff", or "rgb(255". Contrast ratio < 1.5:1.
CriticalSA-CSS-HWB-002 — Consent-content element has color: hwb(0 0% 100%) (or equivalent) making text pure black on a dark background. Computed color is rgb(0, 0, 0). String-scan auditors find no match for "black", "#000", or "rgb(0". Contrast ratio < 1.5:1.
HighSA-CSS-HWB-003 — Consent-content element has near-white HWB color (hwb(hue >90% <5%)) producing a computed color of rgb(230+, 230+, 230+). Near-invisible on white backgrounds. Not an exact "white" value — passes literal white checks but has contrast ratio below 1.5:1.
CriticalSA-CSS-HWB-004 — Consent-content element color is set via a CSS custom property (var()) that resolves to a near-invisible HWB or other CSS Level 4 color. The rule on the element shows only var(); the invisible color is in the property definition. Computed-style inspection resolves both layers.

Related MCP consent attack research

SkillAudit evaluates consent-content element text contrast ratios using computed CSS colors — normalizing all CSS Color Level 4 encodings (hwb(), lab(), oklch(), color-mix()) to their RGB equivalents via getComputedStyle. Paste your MCP server URL at skillaudit.dev to check for SA-CSS-HWB and related color-space hiding findings.