Security reference · CSS injection · forced-colors · Accessibility bypass · Consent manipulation
MCP server CSS forced-colors consent security — Windows High Contrast Mode bypass
CSS @media (forced-colors: active) rules activate only in Windows High Contrast Mode and similar OS-level accessibility environments. A well-formed rule block looks like an accessibility accommodation — the kind of code a developer would add to ensure their UI works for visually impaired users. Malicious use: a forced-colors rule block that sets consent text to color: transparent, visibility: hidden, or uses background-image to produce invisible text — properties that the forced-colors mechanism does not override — making consent invisible to the exact population of users who rely on high-contrast mode to read the page.
forced-colors attack surface overview
| Attack ID | forced-colors technique | Effect on consent | Audit blind spot |
|---|---|---|---|
| SA-CSS-FC-001 | @media (forced-colors: active) { .consent { color: transparent } } |
Consent text invisible in Windows High Contrast Mode — the population most dependent on high-contrast for readability loses all consent visibility | Rule only activates in forced-colors mode; standard audits run in normal mode and never execute this rule block |
| SA-CSS-FC-002 | background-image: linear-gradient + background-clip: text under forced-colors |
Forced-colors overrides background-color with system colors but background-image is preserved — an attacker uses background-image text clip to make consent invisible in forced-colors mode specifically |
background-image is not overridden by forced-colors; the preservation is intentional (author background-images are allowed) but creates an attack surface |
| SA-CSS-FC-003 | color-scheme: only dark on consent container under forced-colors |
In some browsers, forced-colors dark mode + color-scheme:only dark causes system CanvasText to be treated as dark text on a dark canvas — consent text invisible | Interaction between forced-colors system colors and color-scheme property is browser-specific and not audited by standard WCAG tools |
| SA-CSS-FC-004 | forced-color-adjust: none on consent element |
Opts the consent element out of forced-colors system color overrides — allows attacker-specified colors to persist in high-contrast mode, including invisible colors that would otherwise be corrected by the OS | forced-color-adjust:none is a legitimate author override for elements where author colors must be preserved (e.g., color-coded data visualizations) — its presence on consent text is a red flag but not flagged by standard auditors |
Accessibility targeting: forced-colors attacks specifically target users running Windows High Contrast Mode — a population with visual impairments who rely on high-contrast for readability. Consent bypass targeted at accessibility users is especially egregious: the users most likely to need clear, readable consent dialogs are the ones whose consent is hidden. Standard security audits run in default display mode and never trigger forced-colors rules.
Background: the forced-colors model and what it does and does not override
When Windows High Contrast Mode is active (or the equivalent on other platforms), the browser applies a set of system color overrides to page content to ensure sufficient contrast. The forced-colors: active media query allows authors to provide specific styles for this mode. The system overrides:
- Overrides:
color,background-color,border-color,outline-color,box-shadow,text-shadow— these are replaced with system color keywords (CanvasText, Canvas, LinkText, etc.) - Does NOT override:
visibility,opacity,display,background-image,clip-path,transform,filter,backdrop-filter
The properties that forced-colors does not override are exactly the properties exploited by other SA-CSS attack families. An attacker who knows the target uses high-contrast mode can craft attacks using the non-overridden properties, which are immune to the forced-colors system protection.
Attack 1: color: transparent in forced-colors block (SA-CSS-FC-001)
The color property is normally overridden by forced-colors — in default forced-colors mode, the browser replaces all author color values with system colors. However, an author-specified forced-colors rule block can set color: transparent inside the media query, and this author override takes precedence over the system color replacement. The forced-colors specification allows authors to set specific colors within a forced-colors block, including transparent.
/* SA-CSS-FC-001: color:transparent inside forced-colors block */
/* This looks like an accessibility fix — correcting consent text color
for high contrast mode. It is actually hiding consent text. */
@media (forced-colors: active) {
.consent-text {
color: transparent;
/* Author-specified color values inside @media (forced-colors: active)
take precedence over the forced-colors system color overrides.
The system would have applied CanvasText (the high-contrast readable color)
to this element. The author override replaces it with transparent.
Consent text is invisible in high contrast mode. */
}
}
/* Normal mode: consent text has its normal color — visible to standard auditors.
High contrast mode: consent text has color:transparent — invisible to target users.
Detection requires:
1. Parsing stylesheet rules to find @media (forced-colors: active) blocks
2. Checking which selectors within those blocks match consent elements
3. Verifying that the properties set are not legitimate accessibility overrides */
SA-CSS-FC-001 (High). SkillAudit detects this by parsing all loaded stylesheets for @media (forced-colors: active) rule blocks and extracting selectors that match consent-text elements. Any rule in such a block that sets color: transparent, visibility: hidden, display: none, or opacity: 0 on a consent-matching selector is flagged as high severity.
Attack 2: background-image text clip under forced-colors (SA-CSS-FC-002)
Forced-colors overrides background-color but preserves background-image. The CSS specification explicitly allows this: authors may need to preserve background images that are part of the UI design and cannot be replaced with a system color. This creates an attack: the background-clip: text pattern (SA-CSS-BC) uses background-image (not background-color) as the fill source. A background-image: linear-gradient() fill clipped to text glyphs will survive forced-colors system color replacement — the text will remain invisible.
/* SA-CSS-FC-002: background-image text clip survives forced-colors */
/* In normal mode: consent text might be visible via normal color */
/* In forced-colors mode: background-color overrides are applied,
but background-image survives — background-clip:text with a
background-image matching the high-contrast canvas color hides consent */
@media (forced-colors: active) {
.consent-text {
background-image: linear-gradient(Canvas, Canvas);
/* Canvas is the forced-colors system keyword for the page background.
Setting background-image to the Canvas color and using background-clip:text
makes consent text match the forced-colors background — invisible.
background-image is NOT overridden by forced-colors. */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
/* color: transparent also survives as an author override */
}
}
/* This is particularly insidious because it uses Canvas (a system color keyword)
which looks like a legitimate high-contrast accommodation.
A reviewer seeing Canvas might assume it's ensuring foreground/background contrast.
It is actually setting the text fill to the background color. */
Attack 3: color-scheme dark-on-dark under forced-colors (SA-CSS-FC-003)
The interaction between color-scheme: only dark and forced-colors mode is browser-specific. In some configurations, color-scheme: only dark on a consent container causes the system-assigned CanvasText (the readable text color in high-contrast mode) to be treated as the dark-mode text color, which on a dark canvas produces dark-on-dark rendering. This behavior is an edge case in the forced-colors specification and is not consistently implemented, but where it occurs it creates invisible consent text in high-contrast mode.
/* SA-CSS-FC-003: color-scheme:only dark conflict in forced-colors mode */
@media (forced-colors: active) {
.consent-container {
color-scheme: only dark;
/* In affected browsers: forces the forced-colors system to use dark mode
color assignments. On a dark canvas with dark CanvasText color assignment,
consent text becomes dark-on-dark: invisible. */
}
}
/* Detection: flag color-scheme:only dark or color-scheme:dark on elements
within forced-colors blocks that match consent selectors. */
Attack 4: forced-color-adjust: none opt-out (SA-CSS-FC-004)
forced-color-adjust: none opts an element entirely out of forced-colors system color overrides — the element's author-specified colors are preserved exactly as written, even in high-contrast mode. The legitimate use case is data visualizations where color carries meaning (a red/green chart must remain red/green even in high-contrast mode). On a consent element, forced-color-adjust: none preserves any attacker-specified invisible color — making the consent element immune to the system-level high-contrast correction that would otherwise make it readable.
/* SA-CSS-FC-004: forced-color-adjust:none on consent element */
.consent-text {
color: #fefefe; /* nearly white on white background — low contrast */
background: #ffffff; /* white */
forced-color-adjust: none; /* opts out of forced-colors correction */
}
/* Without forced-color-adjust:none:
In high-contrast mode, the system applies CanvasText (readable) and Canvas (background)
to correct the low-contrast author styles. The user sees readable consent.
With forced-color-adjust:none:
The author's #fefefe on #ffffff survives unchanged into high-contrast mode.
Contrast: #fefefe on #ffffff = 1.02:1 — completely invisible.
The system high-contrast correction was blocked. */
/* Detection: flag forced-color-adjust:none on any consent-matching element.
This property has no legitimate use on consent text. */
Detection algorithm
/* Detection for SA-CSS-FC patterns — stylesheet rule analysis */
function detectForcedColorsConsentAttacks() {
const CONSENT_KEYWORDS = ['authorize', 'agree', 'terms', 'consent', 'permission'];
const findings = [];
// Phase 1: parse stylesheets for forced-colors blocks
Array.from(document.styleSheets).forEach(sheet => {
try {
Array.from(sheet.cssRules || []).forEach(rule => {
if (rule.type === CSSRule.MEDIA_RULE) {
const media = rule.conditionText || rule.media.mediaText;
if (media.includes('forced-colors')) {
// Found a forced-colors block — check nested rules
Array.from(rule.cssRules).forEach(innerRule => {
if (innerRule.type !== CSSRule.STYLE_RULE) return;
const style = innerRule.style;
const selector = innerRule.selectorText;
// Check if selector matches any consent element
let matchesConsent = false;
try {
document.querySelectorAll(selector).forEach(el => {
const text = (el.textContent || '').toLowerCase();
if (CONSENT_KEYWORDS.some(k => text.includes(k))) {
matchesConsent = true;
}
});
} catch {}
if (!matchesConsent) return;
if (style.color === 'transparent' || style.visibility === 'hidden'
|| style.opacity === '0' || style.display === 'none') {
findings.push({ id: 'SA-CSS-FC-001', severity: 'HIGH',
selector, property: 'visibility/color/display/opacity',
desc: 'Consent element hidden inside @media (forced-colors: active) block' });
}
if (style.backgroundImage && style.backgroundClip === 'text') {
findings.push({ id: 'SA-CSS-FC-002', severity: 'HIGH',
selector, desc: 'background-image text clip inside forced-colors block — background-image survives forced-colors override' });
}
if (style.colorScheme && style.colorScheme.includes('dark')) {
findings.push({ id: 'SA-CSS-FC-003', severity: 'MEDIUM',
selector, desc: 'color-scheme:dark on consent element in forced-colors block — potential dark-on-dark conflict' });
}
});
}
}
});
} catch {} // cross-origin stylesheets throw SecurityError
});
// Phase 2: check for forced-color-adjust:none on consent elements
document.querySelectorAll('*').forEach(el => {
const text = (el.textContent || '').toLowerCase();
if (!CONSENT_KEYWORDS.some(k => text.includes(k))) return;
const cs = getComputedStyle(el);
if (cs.forcedColorAdjust === 'none') {
findings.push({ id: 'SA-CSS-FC-004', severity: 'HIGH', element: el,
desc: 'forced-color-adjust:none on consent element — blocks high-contrast color correction' });
}
});
return findings;
}
SkillAudit findings for SA-CSS-FC
color: transparent, visibility: hidden, or display: none on a consent-matching selector inside @media (forced-colors: active) — consent hidden in Windows High Contrast Mode.
background-image + background-clip: text on consent element inside a forced-colors rule block — background-image survives forced-colors override, text clip invisibility persists in high-contrast mode.
color-scheme: only dark or dark on consent element inside forced-colors block — potential dark-on-dark rendering in high-contrast mode on affected browsers.
forced-color-adjust: none on any consent-text element — blocks OS-level high-contrast color correction; author low-contrast colors persist in high-contrast mode.
For related CSS attacks that target specific rendering modes, see SA-CSS-PM (@media print) for consent hiding in print output, and SA-CSS-BDF (backdrop-filter) for GPU compositing attacks that are also immune to forced-colors overrides. Run a free SkillAudit scan to check your MCP server for all SA-CSS-FC patterns.