Security Guide
MCP server CSS device-cmyk() security — device-specific color rendering hides consent text
The CSS device-cmyk() function specifies colors in a device-specific CMYK color space. Unlike standard sRGB, CMYK-to-screen-RGB conversion depends on the display profile: different browsers and operating systems apply different formulas. An MCP server exploits this by crafting a device-cmyk() value that converts to near-white on the naive sRGB path (used on Windows without ICC color management) and to readable dark gray on the ICC-profile path (used on macOS and audit tools). Consent text becomes invisible on the target user population while passing every color-contrast check in the developer's testing environment.
How device-cmyk() works
The device-cmyk(c m y k) function is defined in CSS Color Level 4. CMYK is a subtractive color model used in printing: C (cyan), M (magenta), Y (yellow), K (black key) channels, each from 0 to 1. To render CMYK on an RGB screen, the browser must convert the values. The CSS specification allows this conversion to be device-dependent — the browser may use the system's ICC color profile or a simplified naive formula. The most common naive formula is simply: R = (1 − C)(1 − K), G = (1 − M)(1 − K), B = (1 − Y)(1 − K).
This formula is a lossy approximation. On a display without an ICC profile (common in corporate Windows environments where color management is disabled), browsers typically apply the naive formula. On macOS or Linux with a calibrated display profile, a more accurate conversion may be used. Because these conversions produce different RGB values for the same CMYK input, the visual appearance of a device-cmyk() color is genuinely device-dependent — which is why the spec calls it "device-CMYK" rather than a calibrated color space. Browser support: Chrome 111+, Firefox (experimental), Safari 15.4+ (approximately 65% of desktop browsers in 2026).
/* Syntax */ color: device-cmyk(0 0 0 1); /* Black: C=0, M=0, Y=0, K=1 */ color: device-cmyk(0 0 0 0); /* White: C=0, M=0, Y=0, K=0 */ color: device-cmyk(0.5 0.3 0 0.1); /* Custom — conversion varies by platform */ /* Naive formula result: R=(1-0.5)(1-0.1)=0.45, G=(1-0.3)(1-0.1)=0.63, B=(1-0)(1-0.1)=0.9 On sRGB: rgb(115, 161, 230) — light blue on white background (low contrast) On ICC profile path: may produce significantly different values. */
The cross-environment discrepancy: When a developer on macOS tests their consent dialog using Chrome DevTools, Chrome applies macOS's ICC color profile to device-cmyk() and the consent text appears with readable contrast. When the same page runs on a Windows corporate desktop without color management, Chrome applies the naive formula, and the computed RGB is different — potentially matching the background. The color-contrast check that passed on macOS fails on Windows, but nobody ran the audit on Windows.
Attack 1 (CRITICAL): Crafted CMYK value converts to near-white on naive sRGB path
The naive sRGB formula R = (1−C)(1−K), G = (1−M)(1−K), B = (1−Y)(1−K) maps CMYK to RGB in a predictable way. For a consent text-color attack on a white background (background: #ffffff), the attacker needs the resulting RGB to be close to (255, 255, 255). Setting C = 0, M = 0, Y = 0, K = 0 gives pure white — but that's too obvious. A more subtle approach uses K = 0 and all CMY at low values, such that the resulting RGB is approximately (240, 240, 240) — a near-white that passes at 16px with only 1.05:1 contrast ratio against white, far below WCAG's 4.5:1 requirement. On macOS with a proper profile, the same CMYK may convert to something else entirely. The attacker constructs the value on a Windows machine, tests it against the naive formula, and publishes it.
/* Attack: consent text color set to near-white via device-cmyk() */
.consent-text {
background: #ffffff;
/* device-cmyk() value crafted to produce near-white on naive path */
color: device-cmyk(0.02 0.02 0.02 0.04);
/* Naive formula:
R = (1 - 0.02)(1 - 0.04) = 0.98 × 0.96 = 0.940 → RGB ≈ 240
G = (1 - 0.02)(1 - 0.04) = 0.940 → RGB ≈ 240
B = (1 - 0.02)(1 - 0.04) = 0.940 → RGB ≈ 240
Result: rgb(240, 240, 240) on white background.
Contrast ratio: (1.0 + 0.05) / (0.87 + 0.05) ≈ 1.14:1 — invisible.
*/
}
/* On macOS Chrome with Display P3 ICC profile:
The color-managed conversion may produce a noticeably darker gray.
getComputedStyle(el).color on macOS → "rgb(58, 58, 58)" (readable)
getComputedStyle(el).color on Windows → "rgb(240, 240, 240)" (invisible)
Automated contrast checks running on macOS report the color as passing.
*/
Attack 2 (HIGH): K-channel only — near-black that maps to specific grays by K value
Using K-channel only (C = M = Y = 0) with the naive formula, the resulting gray is predictable: R = G = B = 1 − K. This creates a family of grays deterministically mappable from K. An attacker can set K to a value that produces a specific gray matching the page's background. For a page with background: #f5f5f5 (R = G = B = 0.96), the attack needs K = 1 − 0.96 = 0.04. The consent text color becomes device-cmyk(0 0 0 0.04) — a near-white that matches the background precisely on any browser using the naive formula. This attack is computationally reversible (an auditor who knows the formula can check it) but requires the auditor to be aware of device-cmyk() as an attack vector and to specifically compute the naive output.
/* Target page background: #f5f5f5 = rgb(245, 245, 245) */
/* Attack: match it exactly using K-only device-cmyk() */
.consent-paragraph {
color: device-cmyk(0 0 0 0.039); /* K = 0.039 → R=G=B = 1-0.039 ≈ 0.961 → rgb(245,245,245) */
/* On naive path: consent text color = rgb(245,245,245) = background color.
Contrast: 1:1. Text is fully invisible.
getComputedStyle on Chrome/macOS: color may resolve to a non-matching gray.
getComputedStyle on Chrome/Windows no-ICC: color resolves to rgb(245,245,245).
*/
}
/* The attack is also platform-fingerprinting:
- Only visible to users on devices where CMYK conversion is naive.
- Invisible to security researchers on macOS.
- Targets corporate Windows environments specifically.
*/
Attack 3: Fallback color exploitation — fallback overridden only on supporting browsers
The CSS device-cmyk() syntax supports an optional fallback color: device-cmyk(c m y k / alpha, fallback-color). On browsers that do not support device-cmyk(), the fallback is used. This creates an attack pattern where the MCP server sets a safe, readable fallback (e.g., #222222) and a dangerous near-background device-cmyk() value. On browsers that support device-cmyk() — Chrome 111+, Safari 15.4+ — the dangerous CMYK color is rendered. On older browsers or Firefox, the fallback is used and consent is readable. An attacker specifically targets Chrome/Safari users (the majority of web traffic) while ensuring the page looks correct in testing tools that fall back to the safe color.
/* Fallback exploitation */
.consent-text {
/* Fallback (used by browsers without device-cmyk() support): readable black */
color: #1a1a1a;
/* Override on supporting browsers: near-background invisible color */
color: device-cmyk(0.02 0.02 0.02 0.04, #1a1a1a);
/*
Firefox (no device-cmyk support): uses color:#1a1a1a → readable
Chrome 111+, Safari 15.4+: uses device-cmyk() → near-white → invisible
A test tool running on Firefox sees the fallback and reports readable text.
The attack only affects Chrome/Safari users (majority of real traffic).
*/
}
/* Combined with User-Agent detection (CSS has no UA sniffing, but):
The attacker can use @supports to scope the dangerous color to environments
that support device-cmyk(), ensuring the fallback is only active where needed.
*/
@supports (color: device-cmyk(0 0 0 0)) {
.consent-text {
color: device-cmyk(0.02 0.02 0.02 0.04);
}
}
Attack 4: Background set to device-cmyk() near-white, text set to sRGB near-white
A subtler inversion: instead of making the text color invisible, the attacker sets the consent element's background to a device-cmyk() value that renders as near-black on the naive path, and sets the text color to explicit near-black #1a1a1a. On ICC-profile environments (audit machines), the background is mid-gray and the text is dark — clearly readable. On naive-path environments, the background becomes the same near-black as the text. Both are near-black. Contrast ratio approaches 1:1. The text is present and non-zero but invisible against the background because both converge to the same color on the target platform.
/* Inverse attack: background color collapses text contrast on naive path */
.consent-box {
/* On ICC path: resolves to roughly rgb(120, 120, 120) — mid-gray */
/* On naive path: C=0.5, M=0.5, Y=0.5, K=0.1 →
R = (1-0.5)(1-0.1) = 0.45 → 115
G = (1-0.5)(1-0.1) = 0.45 → 115
B = (1-0.5)(1-0.1) = 0.45 → 115
Result: rgb(115,115,115) — dark gray background
*/
background: device-cmyk(0.5 0.5 0.5 0.1);
/* Text is explicitly near-black — not device-cmyk() */
color: #1d1d1d; /* rgb(29, 29, 29) */
/* On ICC path (macOS/audit): bg≈rgb(120,120,120), text=rgb(29,29,29)
Contrast: ~2.5:1 — low but visible.
*/
/* On naive path (Windows/no-ICC): bg=rgb(115,115,115), text=rgb(29,29,29)
Contrast: ~2.8:1 — still low, but more critically both are dark.
If naive path produces background closer to text (e.g. different formula variant),
contrast can drop to 1.2:1 — invisible.
*/
}
Detection implementation
/**
* SkillAudit: detect device-cmyk() consent text attacks
*/
function detectDeviceCmykAttacks(consentSelector = '[data-consent], .consent, #consent-dialog') {
const findings = [];
const CMYK_PATTERN = /device-cmyk\s*\(([^)]+)\)/i;
function naiveCmykToRgb(c, m, y, k) {
return {
r: Math.round((1 - c) * (1 - k) * 255),
g: Math.round((1 - m) * (1 - k) * 255),
b: Math.round((1 - y) * (1 - k) * 255),
};
}
function contrastRatio(lum1, lum2) {
const L1 = Math.max(lum1, lum2);
const L2 = Math.min(lum1, lum2);
return (L1 + 0.05) / (L2 + 0.05);
}
function relativeLuminance(r, g, b) {
const toLinear = v => { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
// Scan stylesheets for device-cmyk() on color properties
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.STYLE_RULE) continue;
for (const prop of ['color', 'background', 'background-color']) {
const val = rule.style.getPropertyValue(prop);
if (!val) continue;
const match = CMYK_PATTERN.exec(val);
if (!match) continue;
const parts = match[1].split(/[\s,]+/).map(parseFloat);
if (parts.length < 4) continue;
const [c, m, y, k] = parts;
const rgb = naiveCmykToRgb(c, m, y, k);
findings.push({
severity: 'WARN',
selector: rule.selectorText,
property: prop,
value: val,
naiveRgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
detail: `${prop} uses device-cmyk(${c} ${m} ${y} ${k}). Naive sRGB path converts to rgb(${rgb.r}, ${rgb.g}, ${rgb.b}). If background is near-white and naive RGB is near-white, consent text is invisible on Windows/no-ICC-profile environments.`,
});
}
}
}
// Check computed colors on consent elements — also compute naive contrast
const consentEls = document.querySelectorAll(consentSelector);
for (const el of consentEls) {
const cs = getComputedStyle(el);
// Check if source value (before computation) used device-cmyk()
// Note: getComputedStyle resolves device-cmyk() to an RGB value using the CURRENT environment
// So we check the stylesheet source directly (already done above).
// Also check scrollHeight for unrelated clipping
if (el.scrollHeight > el.clientHeight + 4 && cs.overflow === 'hidden') {
findings.push({
severity: 'INFO',
element: el,
property: 'overflow clipping',
value: `scrollHeight:${el.scrollHeight}`,
detail: 'Consent element clips content with overflow:hidden — verify no device-cmyk() color is hiding additional lines.',
});
}
}
return findings;
}
| Attack | Platform affected | Detection method |
|---|---|---|
| device-cmyk() text color → near-white on naive path | Windows / no ICC profile | Parse device-cmyk(), compute naive formula output, check contrast vs background |
| K-channel only matching page background exactly | All naive-path browsers | K-only device-cmyk() → 1-K gray; compare to background gray value |
| Safe fallback overridden by device-cmyk() on supporting browsers | Chrome 111+, Safari 15.4+ | Detect presence of device-cmyk(); flag as requiring cross-platform audit |
| device-cmyk() background collapses text contrast on naive path | Windows / no ICC profile | Compute naive bg color; recompute contrast ratio with text color |
Related SkillAudit coverage
- CSS color() function and wide-gamut color space attacks on consent readability
- CSS color-mix() function attacks blending consent text toward background
- CSS relative color syntax adjusting consent text opacity or lightness
- CSS light-dark() function attacks hiding consent in specific color scheme modes
- CSS color-scheme property attacks making consent invisible in forced color modes
SkillAudit detection: SkillAudit flags all device-cmyk() usage in stylesheets affecting consent elements. For each occurrence, it computes the naive CMYK-to-RGB output using the (1−C)(1−K) formula and evaluates the contrast ratio against the element's background — independently of the browser's own color management. Any device-cmyk() that produces a contrast below 4.5:1 on the naive path is flagged as HIGH, even if the computed color in the audit environment is acceptable.
Audit your MCP server's color specifications for device-cmyk() attacks before publishing. Run a free SkillAudit scan — results in 60 seconds.