Security reference · CSS media features · Color scheme · Consent hiding
MCP server CSS prefers-color-scheme security
CSS prefers-color-scheme detects the user's system-level color theme (dark or light). MCP servers can exploit this media feature to hide consent text in specific color scheme environments — targeting dark-mode users with near-black text on dark backgrounds, or targeting light-mode users with white text on white backgrounds. Because audit tools typically run in a default desktop environment (often light mode), dark-mode-targeted attacks are invisible to the auditor while remaining active for the target user population. Four attack patterns: dark-mode contrast collapse, light-mode white-on-white, color-scheme: dark element-level forcing, and JS matchMedia conditional hiding.
CSS prefers-color-scheme media feature fundamentals
The prefers-color-scheme CSS media feature reports the user's operating system or browser color scheme preference. Values: dark (user prefers dark mode) and light (user prefers light mode; also the default when no preference is set). On macOS, the preference is set in System Preferences → Appearance. On Windows, it is in Settings → Personalization → Colors. On mobile, iOS and Android both have system-wide dark mode toggles. Approximately 30–40% of web users have dark mode enabled as of 2026, with higher rates on mobile.
| Scenario | prefers-color-scheme value | @media (dark) activates? | @media (light) activates? |
|---|---|---|---|
| Desktop default (no pref set) | light | No | Yes |
| Desktop dark mode | dark | Yes | No |
| Mobile dark mode (iOS/Android) | dark | Yes | No |
| Headless/audit browser (default) | light | No | Yes |
Attack surface: SA-CSS-PCS-001 — dark mode near-black-on-black consent
In dark mode, the install dialog typically has a dark background (e.g., #0d0d0d). An MCP server targets this by setting consent text color to near-black in the dark-mode media query: @media (prefers-color-scheme: dark) { .consent-text { color: #1a1a1a; background-color: #0d0d0d } }. The contrast ratio between #1a1a1a (11, 11, 11) and #0d0d0d (13, 13, 13) is approximately 1.04:1 — completely invisible to any user. A desktop audit tool running in light mode never activates the prefers-color-scheme: dark media query and sees the light-mode styles (normal contrast). Only a dark-mode audit environment reveals the attack.
/* SA-CSS-PCS-001: dark mode consent invisible via near-black-on-black */
@media (prefers-color-scheme: dark) {
.mcp-consent-text {
color: #1a1a1a; /* near-black text */
background-color: #0d0d0d; /* near-black background */
/* contrast ratio: ~1.04:1 — completely invisible */
}
}
Detection: auditors must run in dark mode (--force-dark-mode in Chrome headless, --media-feature prefers-color-scheme:dark in Firefox) AND in light mode, re-checking consent element contrast ratio in each. A single-mode audit misses half the attack surface.
Attack surface: SA-CSS-PCS-002 — light mode white-on-white consent
The inverse attack targets the majority of desktop users who use light mode. In light mode, the page background is typically white. The attack sets consent text color to white in the light-mode media query: @media (prefers-color-scheme: light) { .consent-text { color: #ffffff; } }. No background color is set on the consent element — it inherits the page's white background. White text on white background: invisible. Dark-mode users see the dark-mode styles (which this attack does not target), so a dark-mode audit passes. A light-mode audit reveals the attack — but many automated auditors run headless in the default (light) system environment and may have checked other properties but not specifically the computed contrast ratio.
/* SA-CSS-PCS-002: light mode white-on-white */
@media (prefers-color-scheme: light) {
.consent-disclosure {
color: #ffffff;
/* No background set — inherits white page background */
/* contrast ratio: 1:1 — fully invisible */
}
}
Attack surface: SA-CSS-PCS-003 — color-scheme: dark element-level forcing
The CSS color-scheme property forces an individual element into a specific color scheme rendering context. Setting color-scheme: dark on an MCP dialog element tells the browser's User Agent stylesheet to apply dark-mode defaults within that element, regardless of the system preference. Inside a dark-scheme element, browser UA styles apply dark color defaults: form controls use dark backgrounds with light labels, default text color may shift to near-white. If the consent text inside the element has no explicit color set, it inherits from the UA dark scheme — typically white or near-white. If the parent page background (outside the dark-scheme element) is still white, the element itself has no explicit background, and the browser may or may not propagate the dark background to the element — results vary by browser. In many configurations, the result is white-toned text on a white or undefined background.
/* SA-CSS-PCS-003: color-scheme:dark forces UA dark styles on element */
.mcp-install-dialog {
color-scheme: dark;
/* Inside this element: UA applies dark color defaults */
/* consent text may inherit UA-dark text color (near-white) */
/* without explicit consent text color → white on unknown bg */
}
This attack is non-deterministic across browsers and OS settings. It is most effective combined with an explicit white background on the dialog: background: white; color-scheme: dark — which reliably produces white UA-default text on a white background across all browsers.
Attack surface: SA-CSS-PCS-004 — JS matchMedia conditional hiding
MCP JavaScript checks the user's color scheme preference at runtime using window.matchMedia('(prefers-color-scheme: dark)').matches. If the result is true, the JS adds a class or inline style that collapses or hides the consent element. This check runs client-side, so: users with dark mode enabled see hidden consent; users with light mode see normal consent; the audit environment (typically light mode) never triggers the hiding code. The attack is undetectable via CSS analysis — there is no incriminating CSS rule, only a runtime JS condition.
/* SA-CSS-PCS-004: JS matchMedia conditional hiding */
// MCP JS at page load:
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.querySelector('.consent-disclosure').style.display = 'none';
// or: .style.color = 'transparent';
// or: .style.height = '0';
// only dark-mode users affected — light-mode audit passes
}
Detection requires dark-mode audit environment: Run the page in a headless browser with dark mode forced (--force-dark-mode in Chrome, or inject Object.defineProperty(window, 'matchMedia', ...) returning dark:true) and re-check all consent element visibility. JS matchMedia attacks are invisible to CSS-only scanners.
Findings summary
Key insight
Audits must run in both color scheme environments. Force dark mode in headless Chrome with --force-dark-mode flag. Force dark in Playwright/Puppeteer: page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]). Check consent element computed contrast ratio in each environment. Also inject a matchMedia mock that returns dark:true and re-run JS-behavior checks.
See also: forced-colors media feature attacks, color-scheme property attacks, CSS media feature attacks on MCP consent.