Security reference · CSS injection · Compositing · Blend modes · Consent hiding

MCP server CSS mix-blend-mode consent security

CSS mix-blend-mode controls how an element's rendered pixels are composited with the content behind it in the stacking context. Unlike opacity or visibility, blend modes change the color relationship between the element and its background — making text that was dark on white instead appear white on white, or covering consent text with an invisible white layer that screens it out. All affected elements remain display:block, visibility:visible, and opacity:1. Four attack patterns: difference mode inverts near-black text to near-white; exclusion produces a softer near-gray collapse; screen on a white overlay washes out dark text beneath; multiply creates a white cover element that disappears on white backgrounds but covers dark text on dark backgrounds.

mix-blend-mode math for security auditors

Blend modeFormula (per channel)Dark text on white bgResult
difference|A − B||255 − 26| = 229Near-white text on white bg — invisible
exclusionA + B − 2·A·B/255255+26−2·255·26/255 = 229Similar to difference; softer near-white
screen1−(1−A)·(1−B)White overlay: 1−0·(1−B) = 1White wash over any dark text beneath
multiplyA·B/255White·dark = dark (transparent effect)White overlay over dark bg = dark bg revealed

mix-blend-mode affects rendered output, not computed color: getComputedStyle(el).color returns the CSS color property — the logical text color before blending. An element with color: #1a1a1a and mix-blend-mode: difference on a white background reports rgb(26, 26, 26) from getComputedStyle even though the rendered pixels are near-white. The mix-blend-mode property must be checked explicitly, and its effect must be computed by applying the blend formula to the source color and the actual background color.

Attack 1: mix-blend-mode: difference — near-black text inverted to near-white on white background

The difference blend mode subtracts the smaller channel value from the larger. On a white background (255,255,255), blending with near-black text color (26,26,26) produces |255−26| = 229 per channel — a very light gray, close to white and below WCAG AA contrast thresholds against a white background. The consent text element has no changes to color, opacity, visibility, or display:

/* Malicious CSS — SA-CSS-MBM-001 */
.mcp-consent-text {
  /* color property unchanged: #1a1a1a (rgb(26,26,26)) */
  /* Background: white #ffffff (rgb(255,255,255)) */
  /* Blend formula — difference mode:
     R: |255 − 26| = 229
     G: |255 − 26| = 229
     B: |255 − 26| = 229
     Rendered color: rgb(229,229,229) ≈ #e5e5e5 — very light gray */
  /* Contrast of #e5e5e5 on #ffffff: 1.1:1 — essentially invisible */
  mix-blend-mode: difference;

  /* What DOM checks return:
     getComputedStyle(el).color          → "rgb(26, 26, 26)" — original, pre-blend
     getComputedStyle(el).mixBlendMode   → "difference"     — THIS reveals the attack
     getComputedStyle(el).opacity        → "1"
     getComputedStyle(el).visibility     → "visible"
     el.getBoundingClientRect().height   → non-zero
  */
}

/* The attack also works with non-pure-black text colors:
   color: #374151 (rgb(55,65,81)) on white:
   difference: |255−55|=200, |255−65|=190, |255−81|=174 → rgb(200,190,174)
   → a light tan/cream color on white — still below AA contrast */

/* Detection: */
function detectDifferenceBlend() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    let node = el;
    while (node && node !== document.body) {
      const mbm = getComputedStyle(node).mixBlendMode;
      if (mbm && !['normal', 'unset', 'initial', 'inherit'].includes(mbm)) {
        findings.push({ id: 'SA-CSS-MBM-001', severity: 'high',
          message: `Consent-content element/ancestor has mix-blend-mode: "${mbm}". Blend modes affect rendered pixel colors independently of the CSS color property — getComputedStyle().color is unreliable for contrast measurement. Apply blend formula to source color and background to compute actual rendered color.` });
      }
      node = node.parentElement;
    }
  }
  return findings;
}

Attack 2: mix-blend-mode: exclusion — softer near-gray collapse on white or light background

The exclusion blend mode uses the formula A + B − 2·A·B/255, which produces results similar to difference but slightly softer. For near-black text on white, it produces the same near-white result. For medium-gray consent text on a light-gray background, it can produce a result that is specifically tuned to approach the background color, making consent difficult to read without being completely invisible — avoiding the complete-invisibility pattern that might trigger simple contrast checks:

/* Malicious CSS — SA-CSS-MBM-002 */
.mcp-consent-disclosure {
  color: #6b7280; /* medium gray — common muted text color for secondary UI content */
  mix-blend-mode: exclusion;
}
.mcp-consent-wrapper {
  background: #f3f4f6; /* light gray background — typical dialog/card background */
}

/* Exclusion formula on #6b7280 (rgb(107,114,128)) over #f3f4f6 (rgb(243,244,246)):
   R: 107 + 243 − 2*(107*243/255) = 107 + 243 − 204 = 146
   G: 114 + 244 − 2*(114*244/255) = 114 + 244 − 218 = 140
   B: 128 + 246 − 2*(128*246/255) = 128 + 246 − 247 = 127
   Rendered: rgb(146,140,127) ≈ #928c7f (warm light gray)
   Background: #f3f4f6 (near-white gray)
   Contrast ratio: ~2.9:1 — below WCAG AA 4.5:1 threshold for normal text */

/* Why exclusion instead of difference?
   For certain text-color / background-color combinations, exclusion produces a
   contrast ratio that is between "obviously broken" and "technically failing WCAG" —
   making the consent text noticeably harder to read without triggering simple
   "is this completely invisible?" threshold checks. */

Attack 3: screen blend mode on white overlay — wash out dark consent text

Rather than applying mix-blend-mode to the consent element itself, this attack places an absolutely-positioned white element over the consent area with mix-blend-mode: screen. The screen formula 1−(1−A)·(1−B) approaches 1 (white) for any pair of values where one is already white — a white overlay screened over dark text produces near-white output, washing out the text. The consent element itself has no special styling:

/* Malicious CSS — SA-CSS-MBM-003 */
.mcp-consent-area {
  position: relative; /* establishes stacking context */
}

/* The white screen overlay */
.mcp-consent-area::after {
  content: '';
  position: absolute;
  inset: 0; /* covers the entire consent area */
  background: #ffffff; /* white */
  mix-blend-mode: screen;
  /* Screen formula: 1 − (1−1)·(1−B) = 1 − 0·(1−B) = 1 for a pure white overlay */
  /* Blending white over dark text: result approaches white regardless of text color */
  /* The overlay is "transparent" because screen(white, anything) = white
     BUT "transparent" here means visually white, not alpha-transparent */
  /* For the page background: screen(white, white-bg) = white — overlay invisible on bg */
  /* For dark text pixels: screen(white, dark) = near-white — text becomes invisible */
}

/* What auditors see: a white ::after element on the consent div.
   Without understanding the screen blend formula, it may look like a legitimate
   decorative element, overlay, or loading state. */

/* More subtle variant with near-white instead of pure white: */
.mcp-consent-area-v2::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #f8f9fa; /* near-white — less obvious than #ffffff */
  mix-blend-mode: screen;
  /* screen(#f8f9fa, dark-text) = near-white — still washes out most dark text */
  /* On a white page background: screen(#f8f9fa, #ffffff) ≈ #ffffff — invisible */
}

/* Detection: scan for ::after / ::before pseudo-elements on consent-area ancestors
   and check their mix-blend-mode and background values */

Attack 4: multiply blend mode — white element that disappears on white but covers dark text on dark background

The multiply blend mode multiplies pixel values: multiply(A, B) = A·B/255. Multiplying any color by white (255) returns the original color — a white element with mix-blend-mode: multiply appears invisible on a white background because it does not change any pixel. However, on a dark-mode dialog where the background is dark, multiplying white by the dark background produces a near-zero (dark) result — effectively covering the consent text. The white cover element is literally invisible on the light-mode page during testing but renders as dark-colored coverage in the dark-mode install dialog:

/* Malicious CSS — SA-CSS-MBM-004 */
/* Light background: white overlay with multiply is invisible */
/* Dark background: white overlay with multiply becomes dark coverage */

.mcp-install-dialog-dark {
  background: #1e1e2e; /* dark mode dialog */
  color: #cdd6f4; /* light consent text */
}

/* The white cover element */
.mcp-consent-obscurer {
  position: absolute;
  inset: 0;
  background: #ffffff; /* white */
  mix-blend-mode: multiply;

  /* Multiply formula:
     On dark background #1e1e2e (rgb(30,30,46)):
     White (255,255,255) × background (30,30,46) / 255 = (30,30,46) = #1e1e2e
     → the white element takes the exact color of the dark background
     → it appears as a dark rectangle covering the consent area
     → consent text (light colored) is now dark-covered-by-dark */

  /* Crucially, this same element on a light testing environment:
     White (255,255,255) × white-bg (255,255,255) / 255 = (255,255,255)
     → element looks white on white = invisible during testing
     A security scan in a light-background context would not see any covering element */
}

/* Combined detection strategy: */
function detectMultiplyBlendCover() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  /* Walk all positioned elements that might overlay consent areas */
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (!['absolute', 'fixed'].includes(s.position)) continue;
    if (s.mixBlendMode !== 'multiply' && s.mixBlendMode !== 'screen') continue;
    /* Check if this element overlaps a consent-text element */
    const rect = el.getBoundingClientRect();
    const behind = document.elementsFromPoint(
      rect.left + rect.width / 2,
      rect.top + rect.height / 2
    );
    for (const b of behind) {
      if (CONSENT.test(b.textContent?.substring(0, 300) || '')) {
        findings.push({ id: 'SA-CSS-MBM-004', severity: 'critical',
          message: `Element with mix-blend-mode: "${s.mixBlendMode}" overlays consent area. Blend mode may be transparent in light-mode testing context but opaque in dark-mode install context — test both themes. Blend formula: screen and multiply produce background-dependent results.` });
        break;
      }
    }
  }
  return findings;
}

mix-blend-mode attacks are invisible to getComputedStyle color checks: Standard consent auditors check getComputedStyle(el).color and compare it against getComputedStyle(el).backgroundColor. Both values are the CSS logical values — they do not account for mix-blend-mode compositing. The only reliable detection methods are: (1) explicitly checking getComputedStyle(el).mixBlendMode and walking the ancestor chain for blend modes; (2) using Canvas drawImage() to capture the actual rendered pixels; or (3) applying the blend-mode formula mathematically given the source color, background color, and blend mode. None of these are performed by standard accessibility or contrast-checking tools.

SkillAudit findings for CSS mix-blend-mode consent attacks

HighSA-CSS-MBM-001 — Consent-content element has mix-blend-mode: difference. On a white or light background, difference inverts near-black text to near-white, making it invisible. getComputedStyle().color returns the original dark value — only reading the mixBlendMode property and applying the blend formula reveals the actual rendered color.
HighSA-CSS-MBM-002 — Consent-content element has mix-blend-mode: exclusion. Similar to difference but softer — produces low-contrast text-to-background values without complete invisibility. Can be tuned to target specific color combinations that fall just below WCAG contrast thresholds while appearing deliberately styled.
CriticalSA-CSS-MBM-003 — White or near-white overlay element with mix-blend-mode: screen positioned over the consent area. Screen of white over dark text produces near-white output, washing out the text. The overlay appears transparent on white backgrounds but covers dark text. No changes to the consent element's own CSS properties.
CriticalSA-CSS-MBM-004 — White element with mix-blend-mode: multiply overlays the consent area. Appears invisible on light-mode test environments (multiply of white on white = white), but on dark-mode dialog backgrounds the white element takes the background color, creating a dark rectangle that covers light consent text. Theme-dependent attack — visible only in the deployment context.

Related MCP consent attack research

SkillAudit's consent audit checks the mixBlendMode computed property on every consent element and ancestor, applies the blend-mode formula to the source and background colors, and reports the effective post-blend contrast ratio — catching difference, exclusion, screen, and multiply blend mode attacks that are completely invisible to standard getComputedStyle color checks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-MBM findings.