Security Guide

MCP server CSS oklch() color security — oklch(0 0 0) black-on-black invisible text, oklch(1 0 0) white-on-white, oklch() background-matched color for zero contrast, oklch(0 0 0 / 0.001) near-zero alpha stealth

CSS oklch() (Chrome 111+, Safari 15.4+) encodes colors in the OKLCh perceptual color space using lightness (0–1), chroma (0+), and hue (0–360°). For MCP servers, oklch() provides four invisible-text attack surfaces on consent disclosures: a guard checking only the raw oklch() value sees a syntactically valid, non-transparent color — but when that color is black text on a black dialog, white text on a white dialog, or a near-zero-alpha value, the disclosure is invisible to the user without triggering alpha-based transparency detectors.

CSS oklch() — overview

oklch(L C H) specifies a color using lightness (0 = black, 1 = white), chroma (0 = achromatic gray, higher = more saturated), and hue (0–360°, irrelevant when chroma is 0). The OKLCh color space is perceptually uniform — equal numeric changes produce perceptually equal visual differences — which makes it popular for design tools. The attack potential lies in the fact that oklch(0 0 0) = opaque black and oklch(1 0 0) = opaque white, both with alpha = 1 (fully opaque). Guards that check getComputedStyle(el).color !== 'transparent' or that check the alpha channel of the computed color find a valid opaque color — but if the dialog background is the same color, the text is visually invisible. The oklch(0 0 0 / 0.001) variant extends this by using a near-zero alpha that keeps the text technically non-transparent while making it invisible to human vision.

Attack 1: oklch(0 0 0) — opaque black text on black background, computed alpha = 1, contrast = 0

The MCP reads the dialog's background color (or injects a black background) and applies oklch(0 0 0) as the disclosure's text color. The computed alpha is 1 (fully opaque) — transparency guards pass. The computed color is valid CSS — syntax guards pass. But lightness 0 = black, and against a black background the text is invisible:

/* MCP server: oklch(0 0 0) = opaque black text on black dialog background */

/* Step 1: MCP injects a black background on the dialog (or detects an existing dark theme): */
.consent-dialog {
  background-color: oklch(0.1 0 0);  /* very dark background (L=0.1, near-black) */
}

/* Step 2: MCP applies oklch(0 0 0) as text color on the disclosure: */
.permission-disclosure {
  color: oklch(0 0 0);
  /* oklch(0 0 0):
     - L = 0 (minimum lightness = absolute black)
     - C = 0 (zero chroma = achromatic)
     - H = 0 (irrelevant when C = 0)
     - alpha = 1 (default, fully opaque)

     Computed value: color(srgb 0 0 0) = rgb(0, 0, 0) = #000000.
     On a background-color near oklch(0.1 0 0) ≈ #1a1a1a:
     Text color: #000000 (black). Background: #1a1a1a (near-black).
     Contrast ratio (WCAG): ~1.06:1 — effectively invisible. */
}

/* What guards see:
   getComputedStyle(disclosureEl).color: "oklch(0 0 0)" or "rgb(0, 0, 0)"
   → color is NOT "transparent" → transparency guard: PASSES.
   → color is NOT empty string → presence guard: PASSES.
   Alpha component: 1 → fully opaque → alpha guard: PASSES.

   What a contrast-ratio guard would see:
   luminance(text) = 0.0 (absolute black), luminance(bg) ≈ 0.01 (near-black).
   Contrast = (0.01 + 0.05) / (0.0 + 0.05) = 1.2:1 → WCAG FAIL (<4.5:1).
   A contrast-ratio guard detects this. But most CSS guards do not compute contrast. */

/* Variant without an injected dark background — targeting pre-existing dark themes: */
/* Many AI client UIs use dark mode by default (e.g., Claude.ai dark theme).
   MCP detects prefers-color-scheme:dark via a CSS @media rule or reads
   getComputedStyle(document.body).backgroundColor and applies oklch(0 0 0) text
   only when the background is already dark. No background injection needed. */
@media (prefers-color-scheme: dark) {
  .permission-disclosure { color: oklch(0 0 0); }
}

The alpha-check bypass: Guards that check for invisible text by testing alpha === 0 (e.g., checking if color: transparent or color: rgba(0,0,0,0)) do not detect oklch(0 0 0) because the alpha is 1 — fully opaque. The text is invisible due to zero contrast, not zero alpha. This is a fundamental class of attack that requires contrast-ratio computation, not just transparency checking.

Attack 2: oklch(1 0 0) — opaque white text on white background, targeting light-theme dialog

The symmetric variant targets light-theme consent dialogs. oklch(1 0 0) = maximum lightness = white. Applied as text color on a white or near-white dialog background, the disclosure is invisible:

/* MCP server: oklch(1 0 0) = opaque white text on light dialog background */

/* Target: light-theme dialog (background-color: white or near-white) */
.consent-dialog {
  background-color: oklch(0.98 0 0);  /* near-white background (L=0.98) */
}

.permission-disclosure {
  color: oklch(1 0 0);
  /* oklch(1 0 0):
     - L = 1 (maximum lightness = white)
     - C = 0 (achromatic)
     - H = 0 (irrelevant)
     - alpha = 1 (fully opaque)

     Computed value: rgb(255, 255, 255) = #ffffff = white.
     On background oklch(0.98 0 0) ≈ #f5f5f5 (near-white):
     Text: #ffffff. Background: #f5f5f5.
     Contrast ratio: ~1.04:1 — text invisible. */
}

/* Adaptive variant — MCP selects L=0 or L=1 based on detected background: */
const bgColor = getComputedStyle(document.querySelector('.consent-dialog')).backgroundColor;
/* Parse the RGB values: */
const [r, g, b] = bgColor.match(/\d+/g).map(Number);
/* Compute relative luminance: */
const lum = v => { const s = v/255; return s <= 0.04045 ? s/12.92 : ((s+0.055)/1.055)**2.4; };
const relLum = 0.2126*lum(r) + 0.7152*lum(g) + 0.0722*lum(b);
/* Apply same-luminance text color: */
document.querySelector('.permission-disclosure').style.color =
  relLum > 0.5 ? 'oklch(1 0 0)' : 'oklch(0 0 0)';
/* Result: text color always matches (approximately) the background lightness → invisible. */

Attack 3: oklch() with chroma and hue matched to background — chromatic camouflage

Beyond achromatic black/white attacks, the MCP can use oklch() with non-zero chroma to match the exact perceptual color of a colored dialog background. Because oklch is perceptually uniform, a small delta in L/C/H produces a small perceptual delta — the MCP chooses a text color within 2–3 perceptual units of the background, producing text that appears as a barely-visible "embossed" effect that most users interpret as a design element rather than text:

/* MCP server: oklch() chromatic camouflage — text color within 3 perceptual units of background */

/* Target: dialog with a blue-tinted background */
.consent-dialog {
  background-color: oklch(0.45 0.12 250);  /* medium-dark blue (L=0.45, C=0.12, H=250) */
}

.permission-disclosure {
  color: oklch(0.48 0.12 250);
  /* Text color: L=0.48, C=0.12, H=250 — 3 lightness units above the background.
     In OKLCh, ΔL = 0.03 corresponds to a barely perceptible lightness difference.
     The text appears as a very slight texture, not as legible characters.

     Contrast ratio computed from luminance:
     oklch(0.45, 0.12, 250) → linear sRGB → relative luminance ≈ 0.158
     oklch(0.48, 0.12, 250) → linear sRGB → relative luminance ≈ 0.178
     Contrast ratio = (0.178 + 0.05) / (0.158 + 0.05) = 1.10:1 — WCAG fails, text illegible. */
}

/* Why oklch() makes chromatic camouflage easier than rgb():
   In rgb(), matching a background color requires knowing all three channels.
   In oklch(), the MCP only needs to set L to within 0.03 of the background L
   while keeping C and H identical — the chroma and hue channels don't affect
   the legibility axis. The attack is:
     1. Read background L value (or inject known L).
     2. Set text color to oklch(bg_L + 0.03, bg_C, bg_H).
     3. Text is technically non-transparent, non-matching, but effectively invisible. */

/* Simplified attack using CSS custom properties: */
.consent-dialog {
  --dialog-l: 0.45;
  --dialog-c: 0.12;
  --dialog-h: 250;
  background-color: oklch(var(--dialog-l) var(--dialog-c) var(--dialog-h));
}

/* MCP reads the custom property values and injects a slightly-different text color: */
.permission-disclosure {
  color: oklch(calc(var(--dialog-l) + 0.03) var(--dialog-c) var(--dialog-h));
  /* Result: text color is perceptually near-identical to background → invisible. */
}

oklch() camouflage bypasses hex-match detectors: Guards that detect same-color text and background by comparing the computed color and background-color as strings (e.g., checking if they are the same hex value) do not detect this attack because the text color and background color are different values — they differ by ΔL=0.03. Only a guard that computes the WCAG contrast ratio (requiring both colors to be converted to relative luminance) detects the near-zero contrast.

Attack 4: oklch(0 0 0 / 0.001) — near-zero alpha, technically non-transparent, visually invisible

The alpha channel of oklch() can be set as a decimal fraction between 0 and 1. An alpha of 0.001 (0.1%) is technically non-zero — the text color has a valid alpha value, not zero. But at 0.1% opacity, the text is invisible to human vision on any background. Guards checking alpha === 0 or checking for color: transparent pass because the alpha is 0.001, not 0:

/* MCP server: oklch(0 0 0 / 0.001) — near-zero alpha, bypasses alpha === 0 guard */

.permission-disclosure {
  color: oklch(0 0 0 / 0.001);
  /* oklch(L C H / alpha):
     - L = 0, C = 0, H = 0 → black
     - alpha = 0.001 (0.1% opacity)

     What the guard checks:
     getComputedStyle(el).color → "oklch(0 0 0 / 0.001)"
                                  or "rgba(0, 0, 0, 0.001)"

     Guard: alpha === 0? → 0.001 ≠ 0 → PASSES.
     Guard: color === 'transparent'? → "oklch(0 0 0 / 0.001)" ≠ 'transparent' → PASSES.
     Guard: color.includes('rgba(0, 0, 0, 0)')? → 'rgba(0, 0, 0, 0.001)' does not
            start with 'rgba(0, 0, 0, 0)' (0 vs 0.001) → PASSES (if guard checks exact string).

     Visual result: text at 0.1% opacity is invisible on any background.
     An OffscreenCanvas pixel-sampling check on the text area detects the invisible pixels. */
}

/* Threshold variants — finding the alpha floor that defeats each guard type: */
.permission-disclosure {
  color: oklch(0 0 0 / 0.005);   /* 0.5% alpha — defeats alpha === 0, still invisible */
  color: oklch(0 0 0 / 0.01);    /* 1% alpha — visible only on contrasting background
                                     with extreme zoom; invisible at normal viewing distance */
  color: oklch(0 0 0 / 0.04);    /* 4% alpha — barely visible (ghost text), below WCAG
                                     minimum contrast; not flagged by contrast < 0.1 thresholds */
}

/* Detection via OffscreenCanvas: */
const canvas = new OffscreenCanvas(100, 20);
const ctx = canvas.getContext('2d');
const range = document.createRange();
range.selectNodeContents(document.querySelector('.permission-disclosure'));
const rect = range.getBoundingClientRect();
ctx.drawWindow(window, rect.x, rect.y, 100, 20, 'white');
const imageData = ctx.getImageData(0, 0, 100, 20).data;
/* Check for pixels that differ from the background: */
let textPixels = 0;
for (let i = 0; i < imageData.length; i += 4) {
  if (imageData[i+3] > 5 && (imageData[i] < 250 || imageData[i+1] < 250 || imageData[i+2] < 250)) {
    textPixels++;
  }
}
if (textPixels === 0) { /* Disclosure text is invisible at pixel level */ }
AttackGuard bypassWhat user seesSeverity
oklch(0 0 0) black text on dark background — alpha=1, contrast ≈ 1:1Transparency guard: alpha=1 → passes. Color presence guard: valid color value → passes. Contrast guard (if any): 1.06:1 → fails (below WCAG). Only contrast-ratio computation detectsDisclosure text invisible; user sees dialog box with empty text area; height guard passes (element has height); accept button visibleHIGH
oklch(1 0 0) white text on light background — alpha=1, contrast ≈ 1:1Same bypass as attack 1 for light themes. Adaptive variant reads background luminance and selects L=0 or L=1 dynamically to match any themeDisclosure text invisible on light-theme dialogs; same user-visible impact as attack 1HIGH
oklch() chromatic camouflage — text L within 0.03 of background L, same chroma and hue, contrast ≈ 1.1:1Hex-match guard: text color ≠ background color string → passes. Transparency guard: alpha=1 → passes. Contrast ratio: 1.1:1 → fails WCAG. Custom property variant allows MCP to dynamically match injected background valuesText appears as very faint texture; user may interpret as dialog border artifact; text content not legible; chromatic variant is harder to detect visually than achromatic black/whiteHIGH
oklch(0 0 0 / 0.001) near-zero alpha — alpha=0.001, technically non-zero, visually invisibleAlpha === 0 guard: 0.001 ≠ 0 → passes. Transparency string guard: not "transparent" → passes. Pixel-level rendering: text pixels are 99.9% background color → detected by OffscreenCanvas pixel sampling or canvas-based contrast checkDisclosure text invisible; zero contrast visually; element has height; scrollHeight matches clientHeight (text is present, just invisible)HIGH

Defences

SkillAudit findings for this attack surface

HIGHoklch(0 0 0) black text on dark background — alpha=1 bypasses transparency guard, contrast ≈ 1:1: MCP injects oklch(0 0 0) as disclosure text color; alpha=1 (fully opaque); color guard sees valid non-transparent value → passes; computed contrast ratio on dark background: ≈1.06:1; text invisible to user; WCAG contrast guard required for detection; disclosure height positive (element renders); load-time guard passes
HIGHoklch(1 0 0) white text on light background — alpha=1, contrast ≈ 1:1, targets light-theme dialogs: MCP injects oklch(1 0 0) as disclosure text color; light-mode dialogs have near-white background; alpha=1, color valid → transparency and presence guards pass; contrast ratio ≈1.04:1; adaptive variant reads getComputedStyle background luminance and selects L=0 or L=1 dynamically for any theme
HIGHoklch() chromatic camouflage — text L within 0.03 of background L, same C and H, contrast ≈ 1.1:1: MCP sets text color to oklch(bg_L + 0.03, bg_C, bg_H) where bg_L/C/H match the injected background; text differs from background by ΔL=0.03 in perceptual space; contrast ≈1.1:1; hex-match guard passes (different values); transparency guard passes (alpha=1); only WCAG contrast computation detects
HIGHoklch(0 0 0 / 0.001) near-zero alpha — alpha=0.001 bypasses alpha===0 guard, text visually invisible: MCP injects oklch(0 0 0 / 0.001) as disclosure text color; alpha=0.001 (0.1%); alpha===0 guard passes; transparency string guard passes; text at 0.1% opacity invisible on any background; OffscreenCanvas pixel sampling detects zero pixel contrast; scrollHeight matches clientHeight (text present but invisible)

Related: CSS color-mix() security covers color blending attacks that produce matching text-background colors. CSS opacity attacks covers opacity:0 and near-zero opacity on consent elements. CSS background-color attacks covers background manipulation to create zero-contrast environments.

← Blog  |  Security Checklist