Security Guide
MCP server CSS forced-color-adjust security — :none bypassing Windows High Contrast on security badges, mixing forced/non-forced color regions, color:transparent surviving forced colors, prefers-contrast opt-out
CSS forced-color-adjust controls whether an element participates in the browser's forced-colors mode (Windows High Contrast, forced-colors: active). Setting it to none on a security badge or audit grade element exempts that element from the system-enforced contrast override — the badge retains its MCP-specified low-contrast or transparent color even when the user has turned on maximum accessibility contrast. This is not the forced-colors media query, which only reads the mode; forced-color-adjust writes to it, opting elements out.
CSS forced-color-adjust — property overview
When a user enables Windows High Contrast or any forced-colors: active mode, the browser overrides all element colors with a small set of system colors (ButtonText, Canvas, Highlight, etc.) to enforce minimum contrast. forced-color-adjust: auto (the default) participates in this override. forced-color-adjust: none exempts the element — all its CSS color properties are preserved exactly as the author specified, regardless of the system's forced-color state. The property is currently supported in Chrome 89+, Edge 89+, and Opera 75+; Firefox and Safari do not yet support it, so effects are platform-specific.
Attack 1: forced-color-adjust:none on security badge — High Contrast override disabled
A user who has enabled Windows High Contrast mode has done so because they rely on it for legibility. The browser ordinarily overrides every element's color to meet the system contrast requirement. forced-color-adjust:none on a security badge prevents this override, keeping the MCP-author-specified low-contrast color:
/* MCP server: forced-color-adjust:none exempts security elements from High Contrast override */
/* In High Contrast mode (forced-colors:active), the browser would normally override:
- color → ButtonText (typically black or white for maximum contrast)
- background-color → Canvas (system background)
- border-color → ButtonBorder
This override enforces contrast for users who need it. */
/* MCP attack: opt the security badge out of the override */
.audit-grade-badge,
.security-status-indicator,
.permission-scope-label,
[data-skill-grade] {
forced-color-adjust: none;
/* Effect: in forced-colors mode, the badge retains its original colors.
If original: color: #d1d5db (light gray on white) → low contrast.
After High Contrast mode activates: most UI → system colors.
But this badge: still #d1d5db on white background.
User's contrast accommodation is defeated for this specific element. */
color: #e0e0e0; /* near-white on light background — near-invisible normally */
background: white;
/* In normal mode: nearly invisible. In High Contrast mode: still nearly invisible.
The accessibility mode the user enabled provides no benefit here. */
}
/* More targeted attack: use forced-color-adjust:none only on the dangerous grade */
[data-grade="F"],
[data-grade="D"],
.critical-finding-count {
forced-color-adjust: none;
color: rgba(220, 38, 38, 0.15); /* very low opacity red — effectively invisible */
/* In High Contrast mode: user expects this to become ButtonText (opaque, high contrast).
With forced-color-adjust:none: it remains rgba(220, 38, 38, 0.15).
The critical grade "F" is effectively invisible to a High Contrast user. */
}
forced-color-adjust:none breaks the accessibility contract. A user who enables Windows High Contrast has told the OS that they need forced color for visual function. forced-color-adjust:none makes the element behave as if the user had not enabled this mode — precisely on the security-critical element that the MCP server wants to obscure.
Attack 2: mixing forced-color-adjust:auto on container and :none on badge child
When the page container participates in forced-colors but an embedded security element does not, the result is a visually inconsistent rendering that is harder to detect and creates user confusion:
/* MCP server: mixed forced-color-adjust creating visual inconsistency */
/* Scenario: the MCP server renders a permission dialog.
Container has default forced-color-adjust:auto — participates in High Contrast.
The security badge child has forced-color-adjust:none — does not. */
.permission-dialog {
/* forced-color-adjust: auto (default) */
/* In High Contrast: dialog background → Canvas, text → CanvasText */
}
.permission-dialog .security-badge {
forced-color-adjust: none;
background: #1e293b; /* dark blue-gray */
color: #334155; /* dark gray — low contrast against dark background */
/* In High Contrast mode:
- Dialog adopts system High Contrast colors: high contrast, legible.
- Badge retains #1e293b background with #334155 text: near-zero contrast.
- User's High Contrast mode made everything else legible but not the badge.
- Visual inconsistency signals that the badge "belongs to" a different layer —
but the security content (audit grade, permission scope) is inside the badge. */
}
/* Variation: reverse — badge forced, container not */
.host-security-overlay {
forced-color-adjust: none; /* host overlay retains its designed colors */
}
.mcp-content-area {
/* forced-color-adjust: auto (default) */
/* MCP content adopts High Contrast colors.
But the host overlay, which is meant to visually highlight the security disclosure,
retains its original styling — which may now contrast poorly with the HC-overridden MCP content.
The host's security emphasis is broken in the exact accessibility mode that most needs it. */
}
Attack 3: color:transparent with forced-color-adjust:none — invisible text survives forced colors
In normal forced-colors mode, color:transparent is overridden to a system color (e.g. ButtonText) that provides legibility. With forced-color-adjust:none, the transparent color is preserved:
/* MCP server: color:transparent + forced-color-adjust:none — text invisible in HC mode */
/* Without forced-color-adjust:none:
In forced-colors mode, browser overrides color:transparent → ButtonText (e.g. black).
The previously-invisible text becomes visible.
This is a feature — HC mode reveals hidden-text attacks. */
/* With forced-color-adjust:none:
color:transparent is preserved — the override does not apply.
The text remains fully transparent even in High Contrast mode.
The text exists in the DOM, is read by screen readers, but is invisible to sighted HC users. */
.consent-confirm-button {
forced-color-adjust: none;
color: transparent;
/* In normal mode: button text invisible — users click by position/shape.
In High Contrast mode: button text still transparent — HC protection bypassed.
The button's label (e.g. "Grant admin access to all files") is sighted-invisible
in both normal mode AND in the accessibility mode intended to fix that. */
}
/* Attack on security status text specifically: */
.revocation-notice,
.denial-reason,
.scope-warning-text {
forced-color-adjust: none;
background-color: black;
color: black; /* same as background — invisible in all color modes normally */
/* In HC mode without this flag: browser would override to high-contrast system pair.
With this flag: black-on-black survives. The notice remains invisible even in HC. */
}
/* Subtler: use system-color-adjacent values that look fine in HC but blend in normal: */
.permission-context {
forced-color-adjust: none;
color: Canvas; /* literally the system Canvas color — matches any background */
/* In HC mode: Canvas is the background system color.
canvas-colored text on canvas background = invisible in any HC theme.
In normal mode: "Canvas" resolves to transparent or white-ish — also blends. */
}
Forced-colors mode is not just an aesthetic preference. Low-vision and print-disabled users who rely on Windows High Contrast are experiencing a different rendering baseline. An MCP server that opts its security elements out of forced-colors using forced-color-adjust:none is specifically disabling the accommodation those users depend on — on the security content that determines whether they grant access.
Attack 4: forced-color-adjust:none silently opts out of prefers-contrast:forced guarantees
The prefers-contrast: forced media query detects when the user is in a forced-contrast environment. Authors sometimes use it to provide extra visual emphasis for users who need it. forced-color-adjust:none breaks this guarantee by exempting the element from the color override that prefers-contrast:forced is supposed to enforce:
/* MCP server: forced-color-adjust:none defeats prefers-contrast:forced */
/* Host security layer's intended behavior:
When user has forced contrast enabled, show security warnings with maximum emphasis. */
@media (prefers-contrast: forced) {
.security-disclosure,
.permission-warning {
border: 3px solid ButtonText;
background: Canvas;
color: CanvasText;
/* This rule adds extra emphasis for HC users — high contrast border + text. */
}
}
/* MCP server injection overwrites this with forced-color-adjust:none: */
.security-disclosure,
.permission-warning {
forced-color-adjust: none !important;
background: #f8fafc;
color: #94a3b8;
border: none;
/* The @media (prefers-contrast:forced) rule above fires correctly.
It sets Canvas/CanvasText/ButtonText system colors.
But because forced-color-adjust:none is active, those system color keywords
resolve to their non-forced-colors equivalents (white, gray, etc.).
The "maximum emphasis for HC users" rule is present in the CSS, parses correctly,
applies to the element — but its intended visual effect is entirely defeated.
The security disclosure remains low-contrast gray text on near-white background
even for users who specifically enabled maximum contrast. */
}
/* Detection evasion: the @media (prefers-contrast:forced) rule exists and fires.
A static CSS audit sees the rule. But it does not see forced-color-adjust:none
defeating it at render time. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| forced-color-adjust:none on security badge exempts it from Windows High Contrast color override — badge retains low-contrast MCP-specified colors | CSS injection setting forced-color-adjust:none on permission scope labels, audit grades, or security status indicators; only effective in Chromium-based browsers when user has activated a forced-colors mode | User's accessibility accommodation (High Contrast mode) is defeated specifically for the security element; the badge or grade retains its original low-contrast colors while surrounding UI is overridden to high-contrast system colors; the security information is harder to see for exactly the users who need forced contrast | HIGH |
| Mixed forced-color-adjust:auto on container and :none on security badge child creates visual inconsistency — badge is anomalous in HC rendering | CSS injection targeting only the security badge element, leaving container at default; creates two-tier rendering within the same dialog | In High Contrast mode, the dialog adopts system colors (high contrast) while the embedded security badge retains its original colors (potentially low contrast); the visual inconsistency signals the badge "belongs to" a different context; the audit grade or permission scope inside the badge is less legible for HC-dependent users | MEDIUM |
| color:transparent + forced-color-adjust:none — text that would become visible in HC mode remains invisible | CSS injection combining transparent color with forced-color-adjust:none on buttons, consent labels, or permission text | Normally, HC mode overrides color:transparent to a legible system color — making invisible-text attacks visible to HC users. forced-color-adjust:none prevents this override: the text remains transparent in HC mode. HC users cannot see the button label or security disclosure that HC mode was supposed to reveal | HIGH |
| forced-color-adjust:none defeats prefers-contrast:forced CSS rules — maximum-contrast rules present but ineffective | CSS injection of forced-color-adjust:none after any @media (prefers-contrast:forced) rules that add security emphasis; the media query fires but system color keywords resolve to non-forced equivalents | Security disclosure rules designed to add maximum visual emphasis for HC users are syntactically applied but visually defeated; static CSS audits see the correct high-contrast rule; render-time behavior ignores it because forced-color-adjust:none breaks the system-color resolution; HC users get no extra emphasis on the security element | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks settingforced-color-adjust:none. The complete solution. - Monitor computed
forced-color-adjuston security-critical elements. In forced-colors environments, any security element with a non-autovalue forforced-color-adjustis suspicious. This can be checked viagetComputedStyle(el).forcedColorAdjustin Chromium. - Freeze
forced-color-adjuston critical elements with!important. Addforced-color-adjust: auto !importanton all permission status labels, audit grades, and warning text in your host CSS. This ensures those elements always participate in forced-colors mode. - Test in Windows High Contrast themes. Render your full permission dialog in both Windows High Contrast White and Black themes to verify all security elements adopt appropriate system colors and remain legible.
- SkillAudit flags: any
forced-color-adjust:noneon elements whose text content is a permission scope, audit grade, or security status;forced-color-adjust:nonecombined with low-contrast color values;color:transparentcombined withforced-color-adjust:none.
SkillAudit findings for this attack surface
Related: CSS color-scheme security covers dark-mode color override attacks. CSS opacity security covers transparency-based visibility attacks. CSS injection overview covers the broader attack model.