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 expression | Equivalent RGB | Background color to make invisible | Stylesheet string scan finds |
|---|---|---|---|
hwb(0 100% 0%) | rgb(255, 255, 255) — white | White or light background | Nothing matching "white", "#fff", "rgb(255" |
hwb(0 0% 100%) | rgb(0, 0, 0) — black | Dark or black background | Nothing matching "black", "#000", "rgb(0" |
hwb(0 95% 0%) | rgb(242, 242, 242) — near-white | White background (nearly invisible) | Nothing matching any white value |
hwb(240 0% 96%) | rgb(0, 0, 10) — near-black blue-black | Dark background | Nothing 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
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.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.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.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
- CSS oklab() attacks — perceptually uniform dark color encoding
- CSS oklch() attacks — hue-chroma-lightness near-invisible colors
- CSS lab() attacks — CIELAB color space invisible text encoding
- CSS color and opacity attacks — transparent and near-zero alpha text
- CSS color-mix() attacks — blending to near-invisible result colors
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.