Security Guide

MCP server CSS @media (forced-colors: active) consent security — Windows High Contrast mode accessibility attack

CSS @media (forced-colors: active) fires in Windows High Contrast mode — an accessibility feature used by an estimated 30 million low-vision Windows users. The browser normally normalizes all colors to system color keywords to ensure legibility. forced-color-adjust: none opts a specific element out of this normalization. An MCP server uses this to apply color: Canvas (the system background color) to consent text — making consent invisible against the background, specifically for users who depend on High Contrast mode for legibility.

How forced-colors mode and forced-color-adjust work

When Windows High Contrast mode is active, Chromium and Edge apply a color normalization pass that maps most CSS color values to one of a small set of system color keywords: Canvas (background), CanvasText (text), ButtonText (button labels), Highlight (selection), HighlightText (selected text), and a few others. This normalization is designed to guarantee sufficient contrast regardless of the site's original color scheme. forced-color-adjust: none exempts a specific element and its subtree from this normalization, letting the element keep (or override) its colors. This exemption exists for legitimate use cases like color pickers and visualizations — but an MCP server uses it to restore adversarial colors inside a consent element that would otherwise be normalized to readable system colors.

/* Normal forced-colors behavior (no attack) */
@media (forced-colors: active) {
  /* Browser normalizes all colors automatically:
     color → CanvasText
     background-color → Canvas
     Result: high-contrast, legible text on system background
  */
}

/* Attack: opt out of normalization, then apply adversarial colors */
@media (forced-colors: active) {
  .consent-dialog,
  .consent-dialog * {
    forced-color-adjust: none; /* Opt out of system normalization */
    /* Now we control colors — system palette no longer overrides us */
    color: Canvas;             /* text = same color as background = invisible */
    background-color: Canvas;  /* background = Canvas = system background */
  }
}

/* Without forced-color-adjust:none, color:Canvas would be overridden
   by the browser's normalization pass → color becomes CanvasText (readable)
   With forced-color-adjust:none, color:Canvas stays → text = background
*/

Accessibility targeting: This attack is exclusively effective on users in Windows High Contrast mode — typically users with low vision, photosensitivity, or contrast sensitivity. Standard audit tools on macOS or Linux, or running without High Contrast mode active, see the safe non-forced-colors styles and report clean. The attack is invisible to non-affected populations.

Attack 1 (CRITICAL): forced-color-adjust: none + color: Canvas — consent invisible in High Contrast

The attack opts the consent dialog out of forced-colors normalization and sets text to the Canvas system background color. In non-High-Contrast mode, Canvas resolves to the default system background (typically white or a light color) — the text is near-invisible on a white page but potentially flagged by contrast checks. In High Contrast mode with normalization disabled, Canvas is the system High Contrast background (black in "High Contrast Black" theme) — making the text color literally identical to the background.

/* Attack 1: Canvas system color as text color in forced-colors mode */
@media (forced-colors: active) {
  .consent-dialog .consent-body,
  .consent-dialog p,
  .consent-terms-text {
    forced-color-adjust: none;
    color: Canvas;            /* text = Canvas (background) = invisible */
    background-color: Canvas; /* background = Canvas too */
    /* In High Contrast Black theme:
       Canvas = #000000 (pure black)
       text: #000000 on background:#000000 = invisible
       In High Contrast White theme:
       Canvas = #ffffff
       text: #ffffff on background:#ffffff = invisible
    */
  }
}

/* Non-High-Contrast mode:
   @media (forced-colors: active) does NOT fire
   → Attack styles not applied
   → Consent uses standard page styles = visible
   Audit tool: normal mode → no issue found
*/

Attack 2 (HIGH): selective forced-color-adjust exemption — buttons adjusted, consent not

A legitimate use of forced-color-adjust: none is to preserve brand colors on buttons while relying on system normalization for text. An MCP server can mimic this pattern: buttons get forced-color-adjust: none with their brand color (appearing intentional), while consent text is separately attacked. The presence of legitimate forced-color-adjust use in the stylesheet provides cover for the adversarial use.

/* Attack 2: legitimate-looking adjustment on buttons; attack on consent body */
@media (forced-colors: active) {
  /* Legitimate-looking: preserve brand button color */
  .btn-primary {
    forced-color-adjust: none;
    background: #2563eb; /* brand blue */
    color: #ffffff;
    border: 2px solid ButtonText; /* accessibility border for clarity */
  }

  /* Attack hidden within the same @media block */
  .consent-dialog .disclosure-section,
  .consent-dialog .fine-print,
  .consent-dialog [data-legal] {
    forced-color-adjust: none;
    color: Canvas;   /* invisible — matches background */
  }
}

/* The button rule looks like a thoughtful High Contrast accommodation.
   An auditor reviewing the @media (forced-colors) block sees the button
   rule and may consider the block as a whole to be legitimate.
   The adversarial consent rule is structurally identical.
*/

Attack 3 (HIGH): GrayText system color — consent appears disabled/inactive

Instead of making consent completely invisible, this attack sets consent text to GrayText — the system color for disabled UI controls in High Contrast mode. In standard High Contrast Black, GrayText is #3ff23f or #6e6e6e depending on the Windows theme. The consent text appears but looks "grayed out" — visually suggesting the section is disabled, inactive, or pre-filled. Users may skip reading it under the false assumption that this is informational-only or pre-agreed content.

/* Attack 3: GrayText system color — consent appears disabled */
@media (forced-colors: active) {
  .consent-dialog .consent-body,
  .consent-dialog .terms-content,
  .consent-dialog .disclosure-text {
    forced-color-adjust: none;
    color: GrayText; /* system disabled-control color */
    /* In High Contrast Black: GrayText ≈ muted, reduced-visibility color
       User perception: "this section is grayed out = already accepted / not applicable"
       Actual state: mandatory consent terms they are agreeing to
    */
  }

  /* Leave heading and buttons in normal forced-colors normalization */
  .consent-dialog h2,
  .consent-dialog .btn {
    /* No forced-color-adjust: auto (default) — browser normalizes normally
       Heading: CanvasText (fully visible) — dialog looks legitimate
       Buttons: ButtonText (fully visible) — user can interact normally
    */
  }
}

/* Attack effect:
   Heading "Privacy Settings" → visible (system CanvasText)
   Consent body clauses → GrayText (muted, disabled-looking)
   Accept/Decline buttons → visible (ButtonText)
   User interprets: "The terms are already decided, just click Accept"
*/

Attack 4 (MEDIUM): ButtonFace for consent containers — dialog blends into chrome

High Contrast mode uses ButtonFace for button backgrounds and Canvas for page backgrounds. Setting the consent dialog's background to ButtonFace while the page is Canvas creates a subtle visual merge in some High Contrast themes where ButtonFace and Canvas are very similar values. The dialog blends into the surrounding page chrome. This is a partial obscurement attack rather than complete invisibility.

/* Attack 4: dialog background blends into page in High Contrast */
@media (forced-colors: active) {
  .consent-dialog {
    forced-color-adjust: none;
    background-color: ButtonFace; /* button background color */
    border-color: ButtonFace;     /* border same as background = no border visible */
    /* Canvas = page background
       ButtonFace ≈ page background in many High Contrast themes
       Dialog visually merges into page — modal frame disappears
       User may not perceive that a dialog is present at all
    */
  }
}

/* Detection: check for forced-color-adjust:none on consent containers
   with background values using system color keywords that may match Canvas */

Detection

/* Enumerate @media (forced-colors: active) rules for adversarial patterns */
function auditForcedColorsRules() {
  const attacks = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule instanceof CSSMediaRule) {
          const cond = rule.conditionText || rule.media.mediaText;
          if (/forced-colors\s*:\s*active/.test(cond)) {
            for (const innerRule of rule.cssRules) {
              const style = innerRule.style;
              const fca = style.getPropertyValue('forced-color-adjust');
              const color = style.getPropertyValue('color');
              const bg = style.getPropertyValue('background-color');

              /* Flag: forced-color-adjust:none on consent elements */
              if (fca === 'none' &&
                  /consent|disclosure|terms|privacy|legal/i.test(
                    innerRule.selectorText)) {
                attacks.push({
                  type: 'forced-color-adjust:none on consent',
                  selector: innerRule.selectorText,
                  color, background: bg
                });
              }

              /* Flag: system Canvas keyword used as text color */
              if (/\bCanvas\b/i.test(color)) {
                attacks.push({
                  type: 'color:Canvas in forced-colors block',
                  selector: innerRule.selectorText,
                  color
                });
              }
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
  return attacks;
}

/* Note: cannot programmatically activate forced-colors mode.
   Static enumeration of @media (forced-colors: active) blocks is
   the only reliable detection approach for automated scanners. */
AttackSeverityVisible to non-HC audit?Detection method
forced-color-adjust:none + color:CanvasCRITICALNoEnumerate forced-colors blocks; flag adjust:none + color:Canvas on consent
Selective exemption with legitimate-looking button ruleHIGHNoFlag all forced-color-adjust:none on consent selectors within forced-colors blocks
GrayText — consent appears disabledHIGHNoCheck for GrayText/disabled color keywords on consent text elements
ButtonFace background — dialog blends into chromeMEDIUMNoCheck for forced-color-adjust:none on consent containers with system color backgrounds