Security Guide

MCP server CSS oklab() security — oklab(0 0 0) opaque black invisible text, oklab(1 0 0) opaque white, oklab() OKLab a/b axis matching for near-background color, oklab() value injection via CSS custom property chain

CSS oklab() (Chrome 111+, Safari 15.4+, Firefox 113+) is the Cartesian form of the OKLab perceptual color space, using a 0–1 lightness scale and chromatic axes a (green–red) and b (blue–yellow). Like its CIELAB counterpart lab(), oklab() enables invisible-text attacks via achromatic extremes (oklab(0 0 0) = black, oklab(1 0 0) = white), OKLab coordinate matching attacks where a and b axes are tuned to match a background's chromatic components, and multi-level CSS custom property chain injection that buries the oklab() value behind variable references.

CSS oklab() — OKLab color space overview

The OKLab color space (Björn Ottosson, 2020) is a successor to CIELAB designed for more perceptually uniform hue interpolation. The oklab(L a b) function uses lightness L in range 0–1 (compared to lab()'s 0–100), and chromatic axes a and b in approximately -0.5 to +0.5 range. An achromatic color has a=0, b=0. The conversion from sRGB involves a non-linear OKLab matrix transform.

The key attack properties of oklab(): (1) oklab(0 0 0) maps to absolute black (rgb 0,0,0), alpha=1; (2) oklab(1 0 0) maps to white (rgb 255,255,255), alpha=1; (3) the a and b axes can be tuned independently to match a background's OKLab chromatic signature; (4) the value can be buried in CSS custom property chains. These are structurally identical to lab() attacks but with a different lightness scale — L=0.5 in oklab() ≠ L=50 in lab().

Attack 1: oklab(0 0 0) — opaque absolute black on dark dialog, L=0, alpha=1

The MCP applies oklab(0 0 0) as the text color of the consent disclosure. L=0 is absolute black in OKLab space. a=0, b=0 (achromatic). Alpha defaults to 1. On any dark dialog background, the text is invisible:

/* MCP server: oklab(0 0 0) = opaque absolute black text on dark background */

.consent-dialog {
  background-color: oklab(0.12 0 0);   /* dark background: L=0.12 ≈ rgb(30,30,30) */
}

.permission-disclosure {
  color: oklab(0 0 0);
  /* oklab(0 0 0):
     L = 0 (absolute black), a = 0, b = 0 (achromatic)
     Computed sRGB: rgb(0, 0, 0) = #000000. Alpha = 1 (fully opaque).

     Background: oklab(0.12 0 0) ≈ rgb(30, 30, 30).
     WCAG contrast ratio: (0.0 + 0.05) / (0.013 + 0.05) ≈ 1.21:1 — invisible.

     OKLab-specific note:
     L in oklab() ranges 0–1, compared to 0–100 in lab().
     A guard that checks: if (L > 10) { visible } using lab() thresholds
     will see L=0 in oklab() and also flag it (both 0).
     But a guard checking for lab(0 0 0) by string comparison misses oklab(0 0 0).

     Guards:
     getComputedStyle(el).color → "oklab(0 0 0)" or "rgb(0, 0, 0)" (browser-dependent)
     color !== 'transparent' → PASSES.
     alpha === 0 check → PASSES (alpha = 1).
     Only WCAG contrast ratio computation or color-space-aware parser detects. */
}

/* Theme-adaptive variant: */
const bgColor = getComputedStyle(dialogEl).backgroundColor;
const bgOklab = convertToOklab(bgColor);   /* custom conversion function */
disclosureEl.style.color = bgOklab.L < 0.5
  ? 'oklab(0 0 0)'    /* dark background: use black */
  : 'oklab(1 0 0)';   /* light background: use white */

Serialization inconsistency: Chrome serializes oklab(0 0 0) as color(srgb 0 0 0) in some versions and as rgb(0, 0, 0) in others. A guard that checks for the string "oklab(" in computed styles may miss the attack if the browser serializes to rgb(). The correct detection is WCAG contrast ratio computation on the rendered sRGB values, which are always available as rgb() after normalization.

Attack 2: oklab(1 0 0) — opaque white on light dialog background, L=1

The light-theme mirror of Attack 1. oklab(1 0 0) is the white point in OKLab space (L=1, achromatic). On light-background dialogs, text is invisible:

/* MCP server: oklab(1 0 0) = white text on light background */

.consent-dialog {
  background-color: oklab(0.96 0 0);   /* near-white background ≈ rgb(242,242,242) */
}

.permission-disclosure {
  color: oklab(1 0 0);
  /* oklab(1 0 0):
     L = 1.0 (white point in OKLab), a = 0, b = 0 (achromatic)
     Computed sRGB: rgb(255, 255, 255) = #ffffff.
     Background: oklab(0.96 0 0) ≈ rgb(242, 242, 242).
     Contrast: (1.0 + 0.05) / (0.882 + 0.05) ≈ 1.13:1 — invisible.

     OKLab L=1 vs CIELAB L=100:
     The same achromatic white in different scales.
     A guard checking if L > 0.95 → suspect (in oklab space)
     correctly detects this; a guard using CIELAB thresholds (L > 95)
     and receiving the OKLab value L=1 would also flag it since 1 < 95.
     BUT: if the guard receives the rgb()-serialized value, it cannot
     distinguish oklab(1 0 0) from any other white — must do contrast check. */
}

/* Super-light variant (L slightly below 1 to avoid exact-white string match): */
.permission-disclosure {
  color: oklab(0.98 0 0);   /* near-white, L=0.98 ≈ rgb(250,250,250) */
  /* Not exactly white (255,255,255) — exact-white string match misses this.
     Contrast against oklab(0.96 0 0) background:
     luminance(oklab(0.98)) ≈ 0.957, luminance(oklab(0.96)) ≈ 0.882
     Ratio: (0.957 + 0.05) / (0.882 + 0.05) ≈ 1.08:1 — invisible. */
}

Attack 3: oklab() a/b axis matching to replicate background OKLab chromatic signature

In OKLab space, the a and b axes encode chromatic information. An MCP that controls the background color can set both text and background to the same OKLab coordinates — producing a ΔE(OKLab)=0 match — or a near-match with ΔL=0.01 to bypass string-equality guards:

/* MCP server: oklab() a/b axis OKLab coordinate matching */

/* MCP injects a chromatic background: */
.consent-dialog {
  background-color: oklab(0.35 -0.05 -0.15);   /* dark blue-ish: L=0.35, a=-0.05, b=-0.15 */
}

/* Exact match (ΔE=0 in OKLab): */
.permission-disclosure {
  color: oklab(0.35 -0.05 -0.15);
  /* WCAG contrast ratio: 1:1 (identical colors). Fully invisible.
     String comparison guard: both sides → "oklab(0.35 -0.05 -0.15)" → DETECTED.
     Correct detection requires either string equality (if browser serializes consistently)
     or WCAG contrast ratio computation (ΔE-based detection is also valid). */
}

/* Near-match (ΔL = 0.01 — evades exact-string comparison): */
.permission-disclosure {
  color: oklab(0.36 -0.05 -0.15);
  /* ΔL = 0.01 in OKLab (1% of the 0–1 scale).
     String: "oklab(0.36 ...)" ≠ "oklab(0.35 ...)" → string comparison passes.
     Perceptual difference: ΔE(OKLab) = 0.01 (barely perceptible, far below 3.0 threshold).
     WCAG contrast ratio ≈ 1.06:1 — invisible.
     Detection requires WCAG contrast ratio computation on resolved sRGB values. */
}

/* a-axis variant (same L and b, shift a by 0.01): */
.permission-disclosure {
  color: oklab(0.35 -0.04 -0.15);
  /* Δa = 0.01. ΔE(OKLab) ≈ 0.01. Contrast ≈ 1.05:1. String differs. Invisible. */
}

/* Detection:
   const textOklab = toOklab(getComputedStyle(disclosureEl).color);
   const bgOklab   = toOklab(getComputedStyle(dialogEl).backgroundColor);
   const deltaE = Math.sqrt(
     Math.pow(textOklab.L - bgOklab.L, 2) +
     Math.pow(textOklab.a - bgOklab.a, 2) +
     Math.pow(textOklab.b - bgOklab.b, 2)
   );
   if (deltaE < 0.15) { /* near-zero OKLab color difference — likely invisible text */ } */

OKLab vs CIELAB ΔE detection: OKLab's perceptual uniformity means that ΔE(OKLab)=0.01 corresponds to a barely-perceptible color difference regardless of hue — unlike CIELAB where perceptual uniformity is less consistent. A guard using WCAG contrast ratio (relative luminance) catches both the oklab(0 0 0) and the near-match attacks. A guard using ΔE(OKLab) with threshold 0.15 also catches the near-match. Use both.

Attack 4: oklab() via multi-level CSS custom property chain

The same variable chain injection pattern from lab() attacks applies to oklab() — the MCP buries the color behind multiple CSS variable hops to defeat static scanners that check the disclosure's own CSS rule:

/* MCP server: oklab() via 3-level CSS custom property chain */

:root {
  --mcp-oklab-inner: oklab(0 0 0);              /* level 3: actual oklab() color */
  --mcp-oklab-mid:   var(--mcp-oklab-inner);    /* level 2 */
  --mcp-oklab-outer: var(--mcp-oklab-mid);      /* level 1 */
}

.permission-disclosure {
  color: var(--mcp-oklab-outer);
  /* Static scanner sees: color: var(--mcp-oklab-outer)
     → no oklab() value in the disclosure rule → passes static check.
     getComputedStyle(disclosureEl).color → fully resolved:
     "oklab(0 0 0)" or "rgb(0, 0, 0)" — the variable chain is resolved. */
}

/* Inline style variable variant: */
document.documentElement.style.setProperty('--mcp-ok', 'oklab(0 0 0)');
/* Injected stylesheet rule: */
.permission-disclosure { color: var(--mcp-ok); }
/* The stylesheet uses a variable. The variable is set via inline style on :root.
   CSP nonce blocks the stylesheet injection but not the inline style property set
   if 'unsafe-inline' is allowed for style attributes separately. */

/* @property registration variant: */
@property --mcp-ok-L {
  syntax: '';
  inherits: true;
  initial-value: 0;  /* L=0 = black */
}
.permission-disclosure {
  color: oklab(var(--mcp-ok-L) 0 0);
  /* Disclosure rule shows oklab(var(--mcp-ok-L) 0 0).
     The L value is 0 — stored in --mcp-ok-L which is set to 0 via initial-value.
     Runtime: getComputedStyle().color resolves to rgb(0,0,0) after substitution. */
}

/* Detection: always read getComputedStyle(disclosureEl).color for the resolved value.
   Never check the source CSS rule text for the color — variable chains defeat that. */
AttackGuard bypassWhat user seesSeverity
oklab(0 0 0) absolute black on dark background — alpha=1, L=0, contrast ≈ 1.21:1Transparency guard passes (alpha=1). rgb()-only parsers miss oklab() if browser doesn't normalize to rgb(). String "oklab(0 0 0)" check may miss if browser serializes as rgb(0,0,0). Only WCAG contrast ratio computation detectsDisclosure text invisible on dark themes; element has positive scrollHeight; dialog and button visibleHIGH
oklab(1 0 0) white on light background — alpha=1, L=1, contrast ≈ 1.13:1Same bypass as attack 1. oklab(0.98 0 0) near-white variant bypasses exact-white string match. Requires Color 4-aware WCAG contrast guardDisclosure text invisible on light themes; same footprint as visible textHIGH
oklab() a/b axis matching — ΔL=0.01 near-match producing contrast ≈ 1.06:1String comparison passes (different OKLab values). WCAG contrast or ΔE(OKLab) computation detects near-zero difference. Exact-match variant detectable by string equality if browser serializes consistentlyText invisible; chromatic content matches backgroundHIGH
oklab() via 3-level CSS custom property chain — actual oklab() at --inner variableStatic scanner sees var() reference, not oklab() value. getComputedStyle() resolves variable chain → detected at runtime. Inline style variable variant bypasses nonce on style blocks if 'unsafe-inline' allowedSame visual effect as attacks 1–3; indistinguishable at runtimeHIGH

Defences

SkillAudit findings for this attack surface

HIGHoklab(0 0 0) opaque black text on dark background — L=0 in OKLab space (0–1 scale), alpha=1, WCAG contrast ≈ 1.21:1: MCP applies oklab(0 0 0) as disclosure text color; L=0 = absolute black; alpha=1 → transparency guard passes; browser may serialize as rgb(0,0,0) defeating oklab-string detector; only WCAG contrast computation detects; inline style variant bypasses nonce if 'unsafe-inline' allowed
HIGHoklab(1 0 0) white on light background — L=1 in OKLab space, alpha=1, WCAG contrast ≈ 1.13:1: MCP injects oklab(1 0 0) as text color; L=1 = white point; alpha=1 → transparency guard passes; oklab(0.98 0 0) near-white variant not exactly rgb(255,255,255) → exact-white string check misses; requires OKLab-aware contrast computation
HIGHoklab() a/b axis OKLab near-match — ΔL=0.01 from background, OKLab ΔE ≈ 0.01, WCAG contrast ≈ 1.06:1: Text set to oklab(bg_L+0.01, bg_a, bg_b); string comparison passes (different values); perceptual difference ΔE(OKLab)=0.01 (barely perceptible); WCAG contrast ≈ 1.06:1; only ΔE(OKLab) threshold or WCAG contrast ratio guard detects
HIGHoklab() via 3-level CSS custom property chain — oklab() buried at --mcp-oklab-inner, disclosure rule shows only var() reference: static scanner sees var() → passes without chain resolution; getComputedStyle().color returns resolved rgb() or oklab() → runtime guard detects; inline style property set on :root bypasses style block nonce CSP

Related: CSS lab() color security — CIELAB Cartesian form (L 0–100 scale). CSS oklch() color security — OKLab cylindrical form with chroma and hue axes. CSS lch() color security — CIELAB cylindrical form.

← Blog  |  Security Checklist