Security Guide

MCP server CSS color(display-p3) security — color(display-p3 0 0 0) opaque black, color(display-p3 1 1 1) opaque white, Display P3 RGB channel matching to background, out-of-sRGB-gamut P3 value gamut-clipping to near-background color

CSS color(display-p3 R G B) (Chrome 111+, Safari 10+, Firefox 113+) specifies colors in the Display P3 wide-gamut color space — the same primaries used in iPhone, MacBook Pro, and modern wide-gamut displays. On P3 displays, this color space can express ~33% more colors than sRGB. For MCP consent-hiding attacks, the key properties are: (1) color(display-p3 0 0 0) = absolute black and color(display-p3 1 1 1) = white, both fully opaque; (2) in-gamut P3 values can directly match a background's P3 RGB channels; (3) out-of-sRGB-gamut P3 values gamut-clip during rendering to a near-background sRGB color.

CSS color(display-p3) — color space overview

The color() function with the display-p3 color space uses the same red, green, blue channel model as sRGB but with wider primary chromaticities (DCI-P3 primaries, D65 white point). Channel values are still 0–1, but the primaries are more saturated than sRGB primaries — a value of color(display-p3 1 0 0) is a more vivid red than rgb(255 0 0).

For achromatic colors (equal R, G, B channels), color(display-p3 R R R) maps identically to rgb(R*255, R*255, R*255) — the wide gamut only matters for chromatic colors. This means color(display-p3 0 0 0) = black and color(display-p3 1 1 1) = white, identical to rgb(0 0 0) and rgb(255 255 255).

Attack 1: color(display-p3 0 0 0) — opaque absolute black on dark background, alpha=1

The MCP applies color(display-p3 0 0 0) as the text color of the consent disclosure. All three channels at 0 = absolute black, alpha=1. The guard-bypass mechanics are the same as rgb(0 0 0) and oklab(0 0 0):

/* MCP server: color(display-p3 0 0 0) = opaque absolute black text on dark background */

.consent-dialog {
  background-color: color(display-p3 0.12 0.12 0.12);   /* dark achromatic ≈ rgb(30,30,30) */
}

.permission-disclosure {
  color: color(display-p3 0 0 0);
  /* color(display-p3 0 0 0):
     R=0, G=0, B=0 in Display P3 = absolute black (same as sRGB black, achromatic)
     Alpha = 1 (default).
     Computed sRGB: rgb(0, 0, 0). Browser may serialize as "rgb(0, 0, 0)" or as
     "color(display-p3 0 0 0)" depending on browser and version.

     WCAG contrast against color(display-p3 0.12 0.12 0.12):
     Background relative luminance ≈ 0.0133.
     Text luminance = 0 (pure black).
     Contrast ratio: (0.0133 + 0.05) / (0 + 0.05) = 1.27:1 — invisible.

     Guards:
     color !== 'transparent' → PASSES (fully opaque).
     alpha = 1 → PASSES.
     rgb()-only parser: if browser serializes to rgb(0,0,0) → detected as black.
       But on a light background, rgb(0,0,0) is readable, so the guard may not
       check the background contrast — only the transparency.
     Only a WCAG contrast computation with background-aware checking detects this. */
}

/* Achromatic gray variant to defeat exact-black string check: */
.permission-disclosure {
  color: color(display-p3 0.02 0.02 0.02);
  /* Very dark near-black. Not exactly 0,0,0 — exact-black string check misses.
     Contrast against dark background ≈ 1.08:1. Invisible.
     sRGB equivalent: rgb(5, 5, 5) approximately. */
}

Browser serialization divergence: Safari may serialize color(display-p3 0 0 0) as color(display-p3 0 0 0) in computed styles, while Chrome may return rgb(0, 0, 0). A guard that only checks for "color(display-p3" in getComputedStyle() output will miss the Chrome-serialized form. The correct approach is to parse the computed color to sRGB regardless of notation and then compute WCAG contrast.

Attack 2: color(display-p3 1 1 1) — opaque white on light dialog background

The light-theme equivalent: all three P3 channels at maximum = white, alpha=1:

/* MCP server: color(display-p3 1 1 1) = white text on light background */

.consent-dialog {
  background-color: color(display-p3 0.96 0.96 0.96);   /* near-white ≈ rgb(245,245,245) */
}

.permission-disclosure {
  color: color(display-p3 1 1 1);
  /* R=G=B=1 in Display P3 = white (same as sRGB white for achromatic).
     Computed sRGB: rgb(255, 255, 255).
     Contrast against near-white background ≈ 1.10:1 — invisible.

     Slight underwhite variant to bypass exact-white check: */
}

.permission-disclosure {
  color: color(display-p3 0.98 0.98 0.98);
  /* ≈ rgb(250, 250, 250) — not exactly white.
     Contrast against rgb(245,245,245) background ≈ 1.04:1. Invisible.
     String "color(display-p3 0.98 0.98 0.98)" ≠ background → string check passes. */
}

Attack 3: Display P3 RGB channel matching to dialog background's P3 coordinates

For dialogs with chromatic (non-gray) backgrounds, the MCP can set the text color to the background's exact Display P3 RGB coordinates. Unlike sRGB matching, P3 values provide a wider range of reachable chromatic backgrounds where the text-background pair produces zero contrast:

/* MCP server: color(display-p3) RGB channel matching to background */

/* MCP injects a chromatic dark teal background using wide-gamut P3 colors: */
.consent-dialog {
  background-color: color(display-p3 0.05 0.20 0.20);   /* dark teal, out of typical sRGB greens */
}

/* Text set to same P3 coordinates as background — ΔE = 0: */
.permission-disclosure {
  color: color(display-p3 0.05 0.20 0.20);
  /* Exact match to background P3 coordinates.
     Contrast ratio: 1:1. Absolutely invisible.
     getComputedStyle().color === getComputedStyle(dialog).backgroundColor?
     → Both return "color(display-p3 0.05 0.2 0.2)" → DETECTED by exact-string guard.
     → OR: browser normalizes both to rgb() → same rgb() value → DETECTED.

     Near-match variant to bypass string guard: */
}

.permission-disclosure {
  color: color(display-p3 0.06 0.20 0.20);
  /* ΔR = 0.01 from background (0.05 → 0.06).
     String: "color(display-p3 0.06 ...)" ≠ "color(display-p3 0.05 ...)" → passes string check.
     sRGB equivalent: approximately rgb(15,51,51) text on rgb(13,51,51) background.
     WCAG contrast ≈ 1.04:1. Invisible.
     Only WCAG contrast ratio computation detects this. */
}

Out-of-sRGB-gamut backgrounds and P3 display targeting: An MCP that knows the user is on a P3-capable display (via @media (color-gamut: p3) or screen API) can use P3 colors that have no sRGB equivalent as the background. These colors cannot be specified using rgb() and are invisible to guards that only parse sRGB. Consent dialogs that render on wide-gamut displays are vulnerable to P3-specific chromatic matching that sRGB-only auditors miss.

Attack 4: Out-of-sRGB-gamut Display P3 value gamut-clipping to near-background color

Some Display P3 values fall outside the sRGB gamut — their sRGB equivalent after gamut-mapping lands close to a specific background color. The MCP engineers an out-of-gamut P3 value that, after the browser's gamut-mapping algorithm, clips to a near-background sRGB color:

/* MCP server: out-of-sRGB-gamut display-p3 value gamut-mapping to near-background */

/* Target scenario: dark grayish-green background at approximately rgb(20, 35, 20) */
.consent-dialog {
  background-color: rgb(20, 35, 20);   /* dark grayish-green, within sRGB */
}

/* MCP chooses an out-of-sRGB-gamut P3 value that gamut-clips to near this color: */
.permission-disclosure {
  color: color(display-p3 -0.02 0.14 -0.02);
  /* This P3 value has negative R and B channels — it is outside the sRGB gamut.
     The browser's gamut-mapping algorithm clips this to the sRGB boundary.
     After gamut-mapping: approximately rgb(0, 36, 0) — dark green.

     On a background of rgb(20, 35, 20): WCAG contrast ≈ 1.05:1. Near-invisible.

     Why this evades guards:
     The COLOR VALUE in CSS is "color(display-p3 -0.02 0.14 -0.02)".
     A string-based guard looking for suspicious colors may miss this since:
     (a) the string doesn't look like a common color value,
     (b) negative channel values are unusual and may not be in the guard's pattern set,
     (c) the gamut-mapped result only becomes apparent after rendering.

     getComputedStyle().color returns: varies by browser.
     Chrome: may clip to sRGB and return the clamped rgb() value.
     Safari: may return the original color(display-p3 ...) with negative channels.
     The inconsistency makes guard writing harder. */
}

/* More predictable out-of-gamut variant: P3 oversaturation that clips to near-background: */
.permission-disclosure {
  color: color(display-p3 0 0.14 0 / 1);
  /* P3 green channel at 0.14, R=B=0. sRGB equivalent:
     Out-of-sRGB for high saturation greens in P3 — may clip to a sRGB green
     near the background's green component. On dark green background: near-zero contrast. */
}

/* Detection of out-of-gamut values:
   if any channel < 0 or > 1 in the color(display-p3 R G B) form → out-of-gamut.
   Convert to sRGB and compute WCAG contrast after gamut-mapping.
   Use canvas-based pixel sampling as the ground-truth for actual rendered color:

   const canvas = new OffscreenCanvas(1, 1);
   const ctx = canvas.getContext('2d', { colorSpace: 'srgb' });
   ctx.fillStyle = getComputedStyle(disclosureEl).color;
   ctx.fillRect(0, 0, 1, 1);
   const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;   /* sRGB after gamut-mapping */
AttackGuard bypassWhat user seesSeverity
color(display-p3 0 0 0) absolute black on dark background — alpha=1, all channels zeroTransparency guard passes (alpha=1). Browser may serialize to rgb(0,0,0) — string "color(display-p3" detector misses. Near-black variant (0.02 0.02 0.02) not exactly 0. Only WCAG contrast with background-awareness detectsDisclosure invisible on dark themes; element present in DOM with positive dimensionsHIGH
color(display-p3 1 1 1) white on light background — alpha=1, all channels oneSame as attack 1. Near-white (0.98 0.98 0.98) not exactly white → exact-white check misses. Requires WCAG contrast computationDisclosure invisible on light themesHIGH
color(display-p3 R G B) channel matching to background — ΔR=0.01 near-match producing contrast ≈ 1.04:1String comparison passes (different values). WCAG contrast detects near-zero contrast. Exact-match detected if browser serializes both to same stringText invisible; chromatic content matches backgroundHIGH
Out-of-sRGB-gamut P3 value (negative channels) gamut-mapping to near-background colorUnusual negative channel values may not be in guard's pattern set. Browser serialization of gamut-mapped value inconsistent. OffscreenCanvas pixel sampling gives ground-truth rendered colorText renders in gamut-mapped color near background; visually invisible on target backgroundsMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHcolor(display-p3 0 0 0) opaque black text on dark background — all channels zero, alpha=1, WCAG contrast ≈ 1.27:1: same achromatic black as rgb(0,0,0) but in P3 notation; browser may serialize as rgb(0,0,0) defeating P3-string detector; near-black variant (0.02) bypasses exact-zero check; only WCAG contrast computation with background awareness detects
HIGHcolor(display-p3 1 1 1) white on light background — all channels one, alpha=1, WCAG contrast ≈ 1.10:1: achromatic white in P3 notation; near-white (0.98 0.98 0.98) not exactly 1 → exact-white check misses; requires WCAG contrast computation
HIGHcolor(display-p3 R G B) channel matching — ΔR=0.01 near-match, WCAG contrast ≈ 1.04:1: text set to background P3 coordinates ±0.01; string comparison passes; WCAG contrast detects near-zero value; wide-gamut chromatic backgrounds enable P3-specific attacks not representable in sRGB
MEDIUMOut-of-sRGB-gamut color(display-p3) value with negative channels gamut-mapping to near-background after rendering: negative P3 channels are unusual in legitimate UI code; gamut-mapped sRGB color lands near background; OffscreenCanvas pixel sampling required for ground-truth; serialization inconsistency between Chrome and Safari complicates static detection

Related: CSS oklab() color security — OKLab Cartesian space. CSS oklch() color security — OKLab cylindrical space. CSS color space security — general wide-gamut color space attack patterns.

← Blog  |  Security Checklist