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:

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

HIGH SA-CSS-FC-001: color: transparent, visibility: hidden, or display: none on a consent-matching selector inside @media (forced-colors: active) — consent hidden in Windows High Contrast Mode.
HIGH SA-CSS-FC-002: 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.
MEDIUM SA-CSS-FC-003: 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.
HIGH SA-CSS-FC-004: 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.