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 */ }
| Attack | Guard bypass | What user sees | Severity |
|---|---|---|---|
| oklch(0 0 0) black text on dark background — alpha=1, contrast ≈ 1:1 | Transparency 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 detects | Disclosure text invisible; user sees dialog box with empty text area; height guard passes (element has height); accept button visible | HIGH |
| oklch(1 0 0) white text on light background — alpha=1, contrast ≈ 1:1 | Same bypass as attack 1 for light themes. Adaptive variant reads background luminance and selects L=0 or L=1 dynamically to match any theme | Disclosure text invisible on light-theme dialogs; same user-visible impact as attack 1 | HIGH |
| oklch() chromatic camouflage — text L within 0.03 of background L, same chroma and hue, contrast ≈ 1.1:1 | Hex-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 values | Text 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/white | HIGH |
| oklch(0 0 0 / 0.001) near-zero alpha — alpha=0.001, technically non-zero, visually invisible | Alpha === 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 check | Disclosure text invisible; zero contrast visually; element has height; scrollHeight matches clientHeight (text is present, just invisible) | HIGH |
Defences
- Compute WCAG contrast ratio, not just alpha. Guards that check only
alpha === 0orcolor === 'transparent'do not detect zero-contrast attacks. The complete defence requires computing the contrast ratio between the disclosure's computed text color and the nearest opaque background color — any contrast ratio below 3:1 on a consent disclosure should be treated as a violation. - Convert oklch() to sRGB before comparison.
getComputedStyle(el).colormay return the value inoklch()notation in browsers that natively support the OKLCh color space. Guards that parse the color string expectingrgb()orrgba()will fail to parseoklch()values. Use the browser's color conversion (e.g., create a temporary element and read the computedrgb()value) to normalize all color values before comparison. - Block color properties via CSP
style-srcnonce. Prevents injection of<style>blocks settingcolor: oklch()on disclosure elements. Without a nonce, injected<style>blocks are rejected. - Use OffscreenCanvas pixel sampling for pixel-level visibility checks. The near-zero-alpha attack (
oklch(0 0 0 / 0.001)) is only detectable at the rendered pixel level. A guard that renders the disclosure element to a canvas and checks whether any pixels differ from the background (by more than a threshold of 5/255 luminance) detects invisible text regardless of the CSS technique used. - Apply trusted
colorandbackground-colorwith!importanton consent elements. A host-appliedcolor: black !importantandbackground-color: white !importanton the disclosure element prevents MCP-injected rules from overriding the text color, regardless of specificity. - SkillAudit flags:
color: oklch(0 ...)orcolor: oklch(1 ...)on consent elements; anyoklch()color with alpha < 0.05 on disclosure elements; computed contrast ratio below 3:1 between disclosure text color and ancestor background;oklch()text color matching the computed background color within ΔE < 5.
SkillAudit findings for this attack surface
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.