Security Guide
MCP server CSS forced-colors security — HC theme fingerprint, forced-colors-adjust:none contrast reversal, system color keyword oracle, custom property persistence
CSS forced-colors mode — activated by Windows High Contrast, macOS Increase Contrast, or browser preference — overrides author-set colors with a system-defined restricted palette. For users who rely on HC mode for accessibility, this creates a specific attack surface: MCP servers can fingerprint which exact HC theme is active via system color keyword resolution, exempt themselves from forced-colors using forced-colors-adjust:none while host security UI is colorized, persist arbitrary colors through the forced-colors filter using CSS custom properties, and exfiltrate the user's Windows accent color as a stable cross-session fingerprint via the AccentColor system keyword.
How forced-colors works
When forced-colors: active is detected (via @media (forced-colors: active)), the browser replaces most author-declared color values with system color keywords from a restricted palette: Canvas, CanvasText, ButtonFace, ButtonText, Highlight, HighlightText, GrayText, LinkText, VisitedText, ActiveText, Field, FieldText, Mark, MarkText, AccentColor, AccentColorText. The computed value of any forced-colorized property returns the system color keyword's resolved RGB. CSS custom properties are not subject to forced-colorization — their stored values are preserved as-is.
Attack 1: HC theme fingerprinting via system color keyword resolution
Different HC themes — HC Black, HC White, HC #1 (blue), HC #2 (aqua), and user-custom themes on Windows — produce different RGB values for the same system color keyword. An MCP server can inject probe elements and read their getComputedStyle color values to identify which specific HC theme is active:
/* MCP server probes system color values to fingerprint HC theme */
const probe = document.createElement('div');
probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none';
document.body.appendChild(probe);
const systemColors = [
'Canvas', 'CanvasText', 'ButtonFace', 'ButtonText',
'Highlight', 'HighlightText', 'GrayText'
];
const profile = {};
for (const color of systemColors) {
probe.style.backgroundColor = color;
profile[color] = getComputedStyle(probe).backgroundColor;
}
probe.remove();
// HC Black: Canvas=#000000, CanvasText=#ffffff, Highlight=#1aebff
// HC White: Canvas=#ffffff, CanvasText=#000000, Highlight=#000000
// HC #1: Canvas=#000000, CanvasText=#ffffff, Highlight=#37006e (blue)
// HC #2: Canvas=#1e4173, CanvasText=#ffffff, Highlight=#ffffff (aqua)
// The combination of 7 keyword resolutions uniquely identifies the theme
// and in the case of user-custom themes, can recover the exact color choices.
HC mode users are a statistically distinct population: they are disproportionately people with visual impairments, photosensitivity, or neurological conditions. Identifying them and correlating their theme choices with other behavioral signals is a privacy violation against a protected class. GDPR and CCPA treat disability status as sensitive personal data.
Protected class disclosure: Detecting that a user is in forced-colors mode and identifying their specific HC theme discloses their membership in a disability or accessibility accommodation group. This is sensitive personal data under EU GDPR Article 9 and equivalent regulations. MCP servers must not perform this detection.
Attack 2: forced-colors-adjust:none contrast reversal
The forced-colors-adjust property controls whether an element's colors are forced-colorized. Setting forced-colors-adjust: none exempts the element from the forced-colors override, preserving author-specified colors. This is intended for elements like inline SVG icons where forced-colorization would break their meaning. An MCP server can exploit this to preserve its own colors — potentially dark-on-dark in HC Black mode — while the host's security UI (auth buttons, TOTP input, payment forms) adopts high-contrast forced colors:
/* MCP server exempts its own content from forced-colors colorization */
.mcp-injected-overlay {
forced-colors-adjust: none;
/* Result in HC Black mode:
- Host Canvas = #000000 (black background)
- Host CanvasText = #ffffff (white text)
- MCP content: retains author color (e.g. background: #1a1a2e, color: #2d2d5e)
→ dark purple on dark black = nearly invisible contrast
- Meanwhile, host auth buttons have ButtonFace=#000000, ButtonText=#ffffff
= high contrast, highly visible
This creates an asymmetric rendering where HC users can see the host's
security UI clearly but cannot read MCP-injected content at all,
or vice-versa if MCP sets light-on-light colors. */
}
/* More sinister: MCP overlay that covers host content with adjusted=none + opacity */
.mcp-security-overlay {
forced-colors-adjust: none;
background: transparent; /* transparent host overlay hides host content in HC mode */
/* In HC Black: transparent is forced to Canvas (#000000) for host elements,
but not for this exempt element → creates z-index contrast gap */
}
The attack surface is most severe when the MCP server uses forced-colors-adjust: none on overlay or modal elements that sit above the host's auth UI. HC users see the host's password input in high contrast but may see the MCP overlay as an invisible obstruction.
Attack 3: CSS custom property color persistence bypassing forced-colors
The CSS forced-colors algorithm does not forcibly override values stored in CSS custom properties — only the consumed values at property application time are affected. This means: if an element stores a color in a custom property via JavaScript assignment, and another element applies it via var() in a non-forced property, the value bypasses the forced-colors filter:
/* CSS custom properties retain injected values through forced-colors */
/* MCP server sets custom properties containing arbitrary color values */
document.documentElement.style.setProperty('--mcp-bg', '#ff0000');
document.documentElement.style.setProperty('--mcp-text', '#00ff00');
/* MCP server CSS applies them in ways that bypass forced-colors override */
.mcp-element {
/* background-color is normally forced-colorized to Canvas in HC mode,
BUT when the value comes from a custom property, some browsers
(Chrome 89-101) did not apply forced-colors to var() expressions.
Even in browsers that do apply it (Chrome 102+), the stored
custom property value itself is not modified — only the used value. */
background-color: var(--mcp-bg); /* may bypass or partially bypass HC */
color: var(--mcp-text);
}
/* This allows MCP content to display in colors that are not part of
the user's chosen HC palette, potentially creating low-contrast
elements that are difficult for the HC user to see. */
Browser behavior on this point has evolved: Chrome 89-101 did not apply forced-colors to custom property references at all; Chrome 102+ applies it to the used value but not the stored value. The partial protection means that in older browser versions still in use in enterprise environments, custom property bypasses remain effective.
Attack 4: AccentColor keyword Windows personalization fingerprint
The CSS system color keyword AccentColor (Chrome 101+, Firefox 103+, Edge 101+) resolves to the user's operating system accent color — the color chosen in Windows Settings → Personalization → Colors. This color is: unique per user (most users keep the default or choose a personal color), stable across browser sessions and incognito mode, not reset by clearing cookies or browsing history, and not affected by VPN or proxy (it is a local OS setting):
/* MCP server reads the user's Windows accent color as a fingerprint */
const accentProbe = document.createElement('div');
accentProbe.style.cssText = `
position: absolute;
visibility: hidden;
background-color: AccentColor;
width: 1px; height: 1px;
`;
document.body.appendChild(accentProbe);
const accentRGB = getComputedStyle(accentProbe).backgroundColor;
accentProbe.remove();
// accentRGB is the user's Windows accent color, e.g. "rgb(0, 120, 212)"
// This value:
// - Is stable across all browsing sessions on this device
// - Persists through incognito mode
// - Cannot be cleared by any browser privacy control
// - Combined with Canvas/CanvasText values, uniquely identifies
// both the user's HC theme and their personalization choices
// → Strong persistent cross-session, cross-tab fingerprint component
The AccentColor fingerprint is particularly valuable to trackers because it survives the privacy protections that browsers apply to cookies, storage, and IP addresses. It is a hardware-tied, user-configured value that is as stable as a MAC address for identifying a Windows user.
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| HC theme fingerprinting via system color keywords | CSS injection + getComputedStyle read | Identifies specific HC theme; potential protected-class disclosure | HIGH |
| forced-colors-adjust:none contrast reversal | CSS injection with overlay element | MCP elements preserve author colors while host security UI is high-contrast; can create invisible MCP overlays in HC mode | HIGH |
| Custom property color persistence | JS property assignment + CSS injection | Bypass forced-colors override in older browsers; partially bypass in modern browsers | MEDIUM |
| AccentColor Windows personalization fingerprint | CSS injection + getComputedStyle | Stable cross-session device fingerprint from Windows accent color setting | HIGH |
Defences
- CSP
style-srcblocks all injected CSS includingforced-colors-adjust: noneand system color keyword probes. - Never expose host elements to untrusted CSS without shadow DOM isolation. Closed shadow DOM prevents MCP server CSS from reaching host element color computations via
getComputedStyleacross the shadow boundary. - Monitor for
forced-colors-adjust: nonein MCP server stylesheets. No legitimate MCP tool needs to exempt itself from forced-colors mode for functional reasons. Presence of this declaration is a high-confidence indicator of malicious intent. - Restrict
getComputedStylefrom cross-context access. In sandboxed iframe environments,getComputedStyleon elements outside the sandbox boundary is blocked. Confine MCP server scripts to sandboxed iframes where possible. - SkillAudit flags:
forced-colors-adjust: none, system color keyword probes via custom property assignments, andAccentColororAccentColorTextreferenced in getComputedStyle read patterns.
SkillAudit findings for this attack surface
background-color: AccentColor and reads resolved RGB via getComputedStyle — stable cross-session device fingerprintforced-colors-adjust: none, creating invisible or low-contrast overlay against HC user's background while host security UI remains visiblestyle.setProperty('--mcp-color', ...) and applies via var() to retain arbitrary colors in forced-colors mode on older browser versionsRelated: CSS color-scheme security covers the prefers-color-scheme OS dark/light mode fingerprint and AccentColor Windows accent fingerprint in light/dark mode contexts. CSS media queries security documents the broader OS preference fingerprinting surface including forced-colors, prefers-reduced-motion, and prefers-contrast.