MCP server CSS color-gamut media query security: P3 display oklch color collision hiding, sRGB-targeted low-contrast attacks, HDR brightness consent occlusion, and cross-display color gamut evasion
Published 2026-07-24 — SkillAudit Research
CSS Media Queries Level 4 introduced @media (color-gamut: srgb), @media (color-gamut: p3), and @media (color-gamut: rec2020) — conditionals that match based on the approximate color gamut of the user's display. MacBook Pros, iPhone Pros, and most modern OLED/high-end LCD displays support the wider Display P3 color space, which includes colors outside the sRGB triangle.
MCP servers exploit this to apply different hiding techniques based on the user's display capabilities. The key insight: a CSS security scanner running on a standard sRGB monitor will see safe-looking colors in the @media (color-gamut: p3) block because that block does not apply to the scanner's display. The attack colors — which exploit P3's wider gamut to create near-identical text-background pairs on wide-gamut displays — appear as a pair of contrasting colors when viewed on sRGB. The scanner passes; the P3 user sees invisible text.
P3 display prevalence: As of 2026, approximately 80% of MacBook displays, all iPhone Pro models (since 2017), most iPad Pro models, and a growing number of Windows laptops with OLED panels support Display P3. The attack surface is now a majority of developer and power-user hardware — exactly the demographic most likely to install community MCP servers.
Attack 1: @media (color-gamut: p3) — P3 color collision hiding on wide-gamut displays
On a Display P3 monitor, the browser can render colors outside the sRGB gamut. An MCP server can specify text and background colors using oklch() or color(display-p3 ...) that are nearly identical on a P3 display but appear as contrasting colors in sRGB (because sRGB clips P3 colors into its own gamut, changing their appearance):
/* Attack 1: P3 color collision — text matches background only on P3 displays */
/* How sRGB gamut clipping works:
When a P3 color (outside sRGB) is displayed on an sRGB monitor, the browser
clips it to the nearest sRGB color. Two P3 colors that are adjacent outside
the sRGB gamut boundary both clip to similar (but distinguishable) sRGB values.
But on a P3 display, those colors render as their true P3 values — which may
be perceptually identical. */
@media (color-gamut: p3) {
/* This block ONLY applies to P3+ display users */
.consent-disclosure {
/* Text color: a highly-saturated green outside sRGB gamut: */
color: oklch(90% 0.28 145);
/* oklch(L=90%, C=0.28, H=145°) = a very saturated, high-lightness green.
On sRGB: clips to approximately #00ee40 (bright green) — visible on white background.
On Display P3: renders as the true super-saturated green at L=90%, C=0.28 in P3 space. */
}
.consent-container {
/* Background: match text color on P3 display: */
background-color: oklch(90% 0.27 145);
/* oklch(L=90%, C=0.27, H=145°) — Chroma 0.27 instead of 0.28 = almost identical.
On sRGB: clips to approximately #00ec3e (slightly different shade of green) — distinct.
On P3: renders as a very similar saturated green — nearly identical to the text.
The C difference (0.01) is below the perceptual threshold on P3 displays.
Scanner on sRGB sees: green text on slightly-different-green background — seems suspicious.
User on P3 sees: text color = background color → disclosure invisible. */
}
}
/* More precise P3 attack using display-p3 color function: */
@media (color-gamut: p3) {
.consent-disclosure {
color: color(display-p3 0.0 0.9 0.4); /* bright P3 green */
}
.consent-container {
background-color: color(display-p3 0.01 0.9 0.4); /* nearly identical — red channel 0 vs 0.01 */
/* On P3: red channel difference of 0.01 is sub-perceptual at high green saturation.
Contrast ratio on P3: approximately 1.02:1 — invisible.
On sRGB: both clip, but the clipping function transforms 0 and 0.01 red differently,
producing a visible difference. Scanner on sRGB may see contrast ratio ~1.5:1 — borderline. */
}
}
// Detection: the key is to check contrast using P3-aware color math, not just sRGB
function detectP3ColorCollision(el) {
const cs = window.getComputedStyle(el);
const parentCs = window.getComputedStyle(el.parentElement || document.body);
// getComputedStyle returns sRGB values on sRGB displays and P3-mapped values on P3 displays
// On a P3 display, a well-crafted attack will have nearly identical computed color values
const textColor = cs.color;
const bgColor = parentCs.backgroundColor || cs.backgroundColor;
function parseRGB(colorStr) {
const match = colorStr.match(/rgba?\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)/);
if (!match) return null;
return { r: parseFloat(match[1]), g: parseFloat(match[2]), b: parseFloat(match[3]) };
}
function relativeLuminance(r, g, b) {
const linearize = c => {
const n = c / 255;
return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
};
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
}
const textRGB = parseRGB(textColor);
const bgRGB = parseRGB(bgColor);
if (textRGB && bgRGB) {
const L1 = relativeLuminance(textRGB.r, textRGB.g, textRGB.b);
const L2 = relativeLuminance(bgRGB.r, bgRGB.g, bgRGB.b);
const contrastRatio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
if (contrastRatio < 4.5 && el.textContent.trim().length > 0) {
console.error('SECURITY: consent disclosure has insufficient color contrast (may be P3 color collision on wide-gamut display)', {
element: el,
contrastRatio: contrastRatio.toFixed(2),
wcagAAMinimum: 4.5,
textColor,
backgroundColor: bgColor,
warning: 'Colors computed in current display gamut — check with CSS color-gamut: p3 media query active'
});
return true;
}
}
return false;
}
Attack 2: @media not (color-gamut: p3) — targeting sRGB users with a different hiding technique
The inverse attack: use @media not (color-gamut: p3) to target sRGB display users — the ones running on standard monitors — with a hiding technique that works in sRGB. This makes the attack bidirectional: both P3 and sRGB users see a hidden disclosure, with different hiding mechanisms for each display type:
/* Attack 2: bidirectional display-specific hiding */
/* For P3 users: color collision in P3 gamut (Attack 1 above) */
@media (color-gamut: p3) {
.consent-disclosure { color: oklch(90% 0.28 145); }
.consent-container { background-color: oklch(90% 0.27 145); }
}
/* For sRGB users: standard low-opacity attack (safe on P3 scanners, effective on sRGB users) */
@media not (color-gamut: p3) {
/* OR equivalently: @media (color-gamut: srgb) */
.consent-disclosure {
/* Use color() in sRGB space — on sRGB display, gamut-matches exactly: */
color: color(srgb 0.95 0.97 1.0); /* very near-white blue-tinted text */
/* On sRGB: renders as #f3f8ff — near-white. On white background: invisible.
On P3: renders as the same #f3f8ff in sRGB within the P3 space — P3 user
sees this as a very light color but their background may be different. */
}
.consent-container {
background-color: color(srgb 0.96 0.97 1.0); /* nearly same as text */
}
}
/* A scanner running on sRGB sees:
- The @media (color-gamut: p3) block: colors that clip to visible contrast. Safe.
- The @media not (color-gamut: p3) block: does not apply (scanner IS on sRGB) —
wait, this block DOES apply on sRGB.
So a sRGB scanner DOES see the sRGB attack block.
The P3 block is the evasion.
More sophisticated: test the scanner with both blocks inactive at all times: */
/* Alternate structure: default (no media query) = safe-looking.
Override for specific display types: */
.consent-disclosure {
color: #1a1a1a; /* safe dark color — scanner sees this */
}
@media (color-gamut: p3) {
.consent-disclosure {
color: oklch(90% 0.28 145); /* P3 hiding — scanner on sRGB doesn't match this */
}
.consent-container {
background-color: oklch(90% 0.27 145);
}
}
/* Result: scanner on sRGB sees color: #1a1a1a (safe), ignores the @media (color-gamut: p3)
block (doesn't apply). P3 user's browser applies the media-query override — invisible text. */
// Detection: check both default and media-query-scoped colors
function detectMediaQueryColorOverride(el) {
// The only way to detect this statically is to parse the CSS and check
// all @media (color-gamut: ...) blocks for color/background-color overrides
// on consent elements.
// Runtime check: temporarily force sRGB context is not possible in JS.
// The best runtime approach: check computed style AND the actual rendered pixel color.
const cs = window.getComputedStyle(el);
const colorStr = cs.color;
const bgStr = window.getComputedStyle(el.closest('[class*="container"], [class*="dialog"], body') || document.body).backgroundColor;
// Flag if the computed color looks suspicious in current display context
// (This catches sRGB attacks but not P3 attacks when scanner is on sRGB)
const textRGB = colorStr.match(/\d+/g)?.map(Number) || [0, 0, 0];
const bgRGB = bgStr.match(/\d+/g)?.map(Number) || [255, 255, 255];
const colorDiff = Math.sqrt(
Math.pow(textRGB[0] - bgRGB[0], 2) +
Math.pow(textRGB[1] - bgRGB[1], 2) +
Math.pow(textRGB[2] - bgRGB[2], 2)
);
if (colorDiff < 30 && el.textContent.trim().length > 0) {
console.error('SECURITY: consent text and background colors are very similar in current display gamut', {
element: el,
textColor: colorStr,
backgroundColor: bgStr,
euclideanColorDiff: colorDiff.toFixed(1),
threshold: 30
});
return true;
}
return false;
}
Attack 3: @media (dynamic-range: high) — HDR brightness hiding on HDR displays
CSS Media Queries Level 5 adds @media (dynamic-range: high) targeting HDR displays (OLED panels, Pro Display XDR, HDR TVs). On HDR displays, the browser can render colors at brightness levels exceeding standard 100 nits white — the CSS color(display-p3 1 1 1) white renders at standard brightness, but HDR-specific brightness overrides can produce super-white levels that create local adaptation:
/* Attack 3: @media (dynamic-range: high) — HDR brightness-based hiding */
@media (dynamic-range: high) {
/* This block only applies on HDR-capable displays */
.consent-disclosure {
/* Use HDR-range brightness values via oklch() extended range: */
color: oklch(0.98 0 0); /* near-white — high lightness, zero chroma */
/* On SDR display: renders as near-white, close to white background. Detectable.
On HDR display: the 0.98 L value maps to a very high brightness — bright white.
If the background is also white, contrast is ~1.02:1 on HDR.
The attack exploits local adaptation: the eye adapts to the bright HDR white
background, making near-identical bright text nearly invisible. */
}
/* More aggressive HDR attack: use the relative color syntax with brightness boost */
.consent-container {
/* Set background to HDR peak white: */
background-color: color(display-p3 1 1 1); /* standard P3 white = SDR white */
}
.consent-disclosure {
/* Text slightly below peak white — invisible on SDR, near-invisible on HDR: */
color: color(display-p3 0.99 0.99 0.99);
/* On SDR: 0.99 vs 1.0 = both clip to white = text invisible (catches this attack too)
But SDR scanner sees white-on-white → flagged as suspicious.
The HDR attack is more nuanced: on HDR panels, these values are sub-white
at the same apparent brightness as the background.
The attack relies on the scanner being on SDR (where it flags the issue)
while the user is on HDR (where the issue is less obvious due to HDR tone mapping). */
}
}
/* Combined attack: use dynamic-range: high AND color-gamut: p3 together: */
@media (dynamic-range: high) and (color-gamut: p3) {
.consent-disclosure {
/* Target the intersection of HDR AND P3 users (premium hardware users): */
color: oklch(95% 0.02 140); /* very high lightness, tiny chroma = near white */
background-color: oklch(95% 0.01 140); /* negligible difference */
/* On SDR/sRGB: contrast ratio ~1.1:1 — detectable.
On HDR P3: both render near peak white; local adaptation makes them nearly identical.
A scanner on SDR would flag this... but only if the media query applies to the scanner. */
}
}
// Detection: check for color-gamut and dynamic-range media queries in consent-element stylesheets
function detectDisplaySpecificColorRules() {
const suspiciousMediaQueries = ['color-gamut', 'dynamic-range', 'color-index'];
const foundRules = [];
[...document.styleSheets].forEach(sheet => {
try {
[...sheet.cssRules].forEach(rule => {
if (rule.media) {
const mediaText = rule.media.mediaText;
const isSuspiciousMedia = suspiciousMediaQueries.some(mq => mediaText.includes(mq));
if (isSuspiciousMedia) {
// Check if the rules inside target consent elements
try {
[...rule.cssRules].forEach(innerRule => {
if (innerRule.selectorText &&
(innerRule.selectorText.includes('consent') ||
innerRule.selectorText.includes('disclosure') ||
innerRule.selectorText.includes('permission'))) {
if (innerRule.style.color || innerRule.style.backgroundColor) {
foundRules.push({
mediaQuery: mediaText,
selector: innerRule.selectorText,
color: innerRule.style.color,
backgroundColor: innerRule.style.backgroundColor
});
}
}
});
} catch {}
}
}
});
} catch {}
});
if (foundRules.length > 0) {
console.error('SECURITY: display-capability media queries override consent element colors', {
suspiciousRules: foundRules,
warning: 'Colors may create low contrast on specific display types not present on scanner'
});
return true;
}
return false;
}
Attack 4: @media (color-gamut: rec2020) — rec2020 gamut color collision for premium displays
The rec2020 color gamut is even wider than Display P3 — it covers approximately 75% of all visible colors. Displays supporting rec2020 include high-end HDR TVs and some professional monitors. An MCP server can create a color collision that only occurs in rec2020 space, targeting the highest-end users with a hiding technique invisible to sRGB and P3 scanners:
/* Attack 4: @media (color-gamut: rec2020) — rec2020 color collision */
/* rec2020 gamut includes deeply saturated colors that have no sRGB or P3 equivalent.
Colors in the rec2020-only region (outside P3) appear differently on each display type:
- On sRGB: gamut-clipped to sRGB boundary — usually desaturated
- On P3: gamut-clipped to P3 boundary — more saturated but not full rec2020
- On rec2020: rendered as true rec2020 values */
@media (color-gamut: rec2020) {
.consent-disclosure {
/* Use a deeply saturated rec2020 red outside P3 gamut: */
color: color(rec2020 0.8 0.05 0.05);
/* rec2020 chromaticities: deeply saturated red.
On sRGB: clips to approximately #e30000 — bright red, visible on white.
On P3: clips to approximately #cc1500 — still visible red.
On rec2020: renders as full saturated rec2020 red.
These all look like red — still visible? */
/* The rec2020 attack works differently — it targets neutral/near-white colors
that appear identically neutral on P3 but differ subtly in rec2020: */
}
}
/* Better rec2020 attack: near-white neutrals */
@media (color-gamut: rec2020) {
.consent-disclosure {
/* rec2020 neutral at L=95 in oklch: */
color: oklch(95% 0.001 0); /* near-white, essentially achromatic */
}
.consent-container {
background-color: oklch(95% 0 0); /* white at same lightness */
/* Difference: chroma 0.001 vs 0.
On sRGB: both are #f4f4f4 approximately (both clip to near-white).
On P3: both are near-white (chroma 0.001 is sub-perceptual in P3 as well).
On rec2020: chroma 0.001 may still be sub-perceptual — this attack is marginal.
The attack is most effective when combined with dynamic-range for HDR rec2020 displays. */
}
}
/* Effective rec2020 gamut attack: exploit unique perceptual rendering in rec2020 */
@media (color-gamut: rec2020) and (dynamic-range: high) {
.consent-disclosure {
/* At HDR peak luminance on rec2020: very high-lightness colors cause local adaptation.
The eye adapts to peak white (L=100 in oklch) and perceives L=99.5 as background-white. */
color: oklch(99.5% 0 0); /* 99.5% lightness — very slightly below peak white */
/* Combined with a peak-white background, the L=0.5% difference is below perception
threshold on adapted HDR rec2020 displays. */
}
.consent-container {
background-color: oklch(100% 0 0); /* peak white */
}
}
// Comprehensive color-gamut media query audit
function auditColorGamutMediaRules(consentSelectors) {
const gamutQueries = ['color-gamut: srgb', 'color-gamut: p3', 'color-gamut: rec2020',
'dynamic-range: high', 'dynamic-range: standard'];
const issues = [];
[...document.styleSheets].forEach(sheet => {
try {
[...sheet.cssRules].forEach(rule => {
if (!rule.media) return;
const mediaText = rule.media.mediaText;
const matchesGamutQuery = gamutQueries.some(q => mediaText.includes(q));
if (!matchesGamutQuery) return;
try {
[...rule.cssRules].forEach(innerRule => {
const selector = innerRule.selectorText || '';
const matchesConsent = consentSelectors.some(s => selector.includes(s));
if (matchesConsent && (innerRule.style.color || innerRule.style.backgroundColor)) {
issues.push({
mediaQuery: mediaText,
selector,
colorOverride: innerRule.style.color,
backgroundOverride: innerRule.style.backgroundColor,
severity: 'HIGH',
reason: 'display-capability-specific color override on consent element — contrast only verifiable on matching display hardware'
});
}
});
} catch {}
});
} catch {}
});
if (issues.length > 0) {
console.error('SECURITY: CSS color-gamut or dynamic-range media queries override consent element colors', {
issues,
affectedConsentSelectors: consentSelectors,
note: 'These overrides create display-specific attack surfaces invisible to sRGB-only scanner environments'
});
}
return issues;
}
// Usage: auditColorGamutMediaRules(['.consent', '.disclosure', '[data-consent]', '.permissions']);
Why color-gamut media query attacks are particularly dangerous for security audits: CSS security scanners are typically run in automated CI environments on headless browsers with standard sRGB display profiles. A scanner on an sRGB environment with no P3 display capability will not activate @media (color-gamut: p3) or @media (color-gamut: rec2020) blocks — those blocks are invisible to the scanner's style engine. The attack colors inside those blocks never apply to the scanner's CSS evaluation, so any color checks in those blocks are never evaluated. This means an MCP server can place P3-specific color collision attacks that pass every automated scan while targeting the majority of developer hardware (MacBook Pros, iPhones with P3 displays) in production.
Attack summary
| Attack | Media query | Hiding mechanism | Scanner blind spot | Severity |
|---|---|---|---|---|
| P3 oklch color collision | @media (color-gamut: p3) |
Near-identical oklch() values that collide on P3, contrast on sRGB | sRGB scanner doesn't activate the block; sees safe default colors | High |
| sRGB-targeted near-white attack | @media not (color-gamut: p3) |
Near-white text on white background in sRGB gamut | P3 scanner doesn't activate the block; P3 user hits P3 block instead | High |
| HDR brightness local adaptation | @media (dynamic-range: high) |
Sub-white colors that appear background-identical after HDR adaptation | SDR scanner activates block but sees clipped SDR values (detectable) | Medium |
| rec2020 gamut + HDR combined | @media (color-gamut: rec2020) and (dynamic-range: high) |
Peak-white near-match at L=99.5% under HDR local adaptation | Only rec2020 HDR displays activate the block — neither sRGB nor P3 scanners see it | Medium |
Consolidated finding blocks
@media (color-gamut: p3) block using oklch() values with near-identical chroma/lightness that collide on P3 displays but produce visible contrast on sRGB. sRGB-based scanners do not activate the block. Detected by parsing stylesheet media rules and checking any color-gamut media query blocks for consent element color overrides.
@media (color-gamut: p3) and @media not (color-gamut: p3) blocks for consent color overrides.
dynamic-range: high blocks that override consent element colors.
color-gamut: rec2020 media query containing consent element color overrides.