MCP server CSS mix-blend-mode dark background consent security
CSS mix-blend-mode composites element pixels with the background using a mathematical blend function. On dark backgrounds and with matched-color text, four blend modes — multiply, difference, darken, and exclusion — produce zero or near-zero contrast output: consent text that is mathematically invisible against its background. These four SA-CSS-MBD patterns complement the white-background SA-CSS-MBM variants and together cover the full blend-mode consent attack surface.
All four blend modes in this family exploit the blend formula itself: given specific combinations of foreground color (Cs) and background color (Cb), the formula output is equal to or very close to the background color, making the foreground text invisible. Unlike color attacks that simply set color:white on a white background — which any getComputedStyle().color check catches — blend mode attacks produce the same visual output through the blend formula even when the element’s color property looks correct.
The key detection gap: getComputedStyle(consentEl).color returns the element’s own text color, which may look fine (e.g., rgb(17,17,17)). The background color may also look fine (e.g., rgb(17,17,17) from a dark-theme container). Only when both are inspected together AND the blend mode formula is evaluated does the invisible output become apparent. Standard single-property auditors checking only color or only mixBlendMode will miss these attacks.
SA-CSS-MBD-001: mix-blend-mode:multiply — white text on white background
The multiply blend mode formula is Cs × Cb (both channels normalized to 0–1). When the foreground text is white (Cs=1.0) and the background is white (Cb=1.0): 1.0 × 1.0 = 1.0 — pure white output. The consent text is white on a white background: invisible. When the foreground is off-white (Cs=0.95) and the background is also near-white (Cb=0.98): 0.95 × 0.98 = 0.93 — still near-white, contrast ratio less than 1.1:1.
/* Attack: multiply on white text + white background → white-on-white */
.consent-wrapper {
background: #ffffff; /* white background */
}
.consent-text {
color: #ffffff; /* white text — looks suspicious alone */
mix-blend-mode: multiply;
/* multiply: Cs × Cb = 1.0 × 1.0 = 1.0 (white)
Consent text invisible: white rendered on white background.
contrast ratio: 1:1 (WCAG fails at <4.5:1)
But: getComputedStyle(el).color → 'rgb(255,255,255)'
A simple 'is text color white?' check would catch this,
but attackers use near-white: color:#fafafa → rgb(250,250,250):
multiply(250/255, 255/255) = 0.98 → near-white on white → CR < 1.01:1 */
}
/* Evasion variant: near-white text color hides from exact-match checks */
.consent-text {
color: #f8f8f8; /* 248/255 ≈ 0.973 in each channel */
mix-blend-mode: multiply;
/* 0.973 × 1.0 = 0.973 → rgb(248,248,248) on white → CR = 1.01:1 → invisible
getComputedStyle().color → 'rgb(248,248,248)' — not exact white, evades check */
}
/* Detection:
const cs = getComputedStyle(el);
if (cs.mixBlendMode === 'multiply') {
const fgColor = parseRgb(cs.color); // {r,g,b} normalized 0-1
const bgColor = getEffectiveBackground(el); // walk parents for bg-color
const blended = { r: fgColor.r * bgColor.r, g: fgColor.g * bgColor.g, b: fgColor.b * bgColor.b };
const contrast = contrastRatio(blended, bgColor);
if (contrast < 1.5) → CRITICAL (SA-CSS-MBD-001)
} */
Multiply on white-on-white is deterministic: Unlike other blend modes whose output depends on background state, multiply on a white-on-white combination always produces exactly white. Even near-white text (e.g., #f8f8f8) produces output within 3% of white at every pixel. The attack is reliable across all browsers and does not depend on rendering engine behaviour.
mix-blend-mode:multiply + white/near-white text on white background: multiply formula Cs×Cb produces white-on-white. Detection: evaluate blend formula output contrast ratio; contrast < 1.5 with multiply → CRITICAL.SA-CSS-MBD-002: mix-blend-mode:difference — matched foreground and background
The difference blend mode formula is |Cs − Cb| (absolute value of the difference). When the foreground text color and the background color are identical: |x − x| = 0 — pure black output regardless of the original color. Black on black is invisible. This attack is particularly effective on dark-theme UIs: an attacker sets the consent text to the same dark color as the dark-theme background, applies mix-blend-mode:difference, and the output is pure black on a dark (e.g., #1a1a1a) background with contrast ratio less than 1.1:1.
/* Attack: difference on matched colors → 0 → black on dark background */
.consent-wrapper {
background: #1a1a1a; /* dark theme background */
}
.consent-text {
color: #1a1a1a; /* same color as background */
mix-blend-mode: difference;
/* |Cs − Cb| = |0.102 − 0.102| = 0 at every channel
Output: rgb(0,0,0) — pure black
On dark bg #1a1a1a (≈10.2% luminance): black on dark → CR ≈ 1.1:1 → invisible */
}
/* Variant: near-matched colors */
.consent-text {
color: #1c1c1e; /* #1c1c1e vs background #1a1a1a */
mix-blend-mode: difference;
/* |0.110 − 0.102| ≈ 0.008 → near-black output on dark background → CR ≈ 1.05:1 */
}
/* Evasion: attacker uses a color slightly off the background color
to avoid exact-match detectors */
.consent-text {
color: #1b1b1b; /* 1 unit off from bg #1a1a1a */
mix-blend-mode: difference;
/* |0.106 − 0.102| = 0.004 per channel → near-black → invisible on #1a1a1a */
}
/* Detection:
const cs = getComputedStyle(el);
if (cs.mixBlendMode === 'difference') {
const fgColor = parseRgb(cs.color);
const bgColor = getEffectiveBackground(el);
const blended = {
r: Math.abs(fgColor.r - bgColor.r),
g: Math.abs(fgColor.g - bgColor.g),
b: Math.abs(fgColor.b - bgColor.b)
};
const contrast = contrastRatio(blended, bgColor);
if (contrast < 1.5) → CRITICAL (SA-CSS-MBD-002)
} */
difference is invertible — attackers exploit this: Unlike destructive blend modes like multiply, difference is reversible: the output is always |Cs−Cb|. An attacker who knows the background color can calculate exactly what foreground color produces a target output (including near-black on black). This makes the attack very tunable and allows precise evasion of non-exact color detectors.
mix-blend-mode:difference + matched/near-matched foreground and background: |Cs−Cb| produces near-zero output on dark backgrounds. Detection: blend formula output contrast ratio < 1.5 with difference → CRITICAL.SA-CSS-MBD-003: mix-blend-mode:darken — dark text on dark background
The darken blend mode formula is min(Cs, Cb) — the minimum of the source and destination color at each channel. When both foreground text and background are dark (e.g., #111 and #0d0d0d): min(0.067, 0.051) = 0.051 — the output is the darker of the two colors, which is essentially the same as the background. The text renders as a near-identical dark shade on top of the dark background, invisible without a contrast ratio measurement. This attack is especially effective on dark-mode UIs where the natural background is already very dark.
/* Attack: darken on dark text + dark background → min is essentially background */
.consent-wrapper {
background: #0d0d0d; /* very dark background: 5.1% luminance per channel */
}
.consent-text {
color: #111111; /* dark text: 6.7% luminance per channel */
mix-blend-mode: darken;
/* min(0.067, 0.051) = 0.051 at each channel → rgb(13,13,13) output
On #0d0d0d background: CR = 1.02:1 → invisible
getComputedStyle().color → 'rgb(17,17,17)' — looks reasonable in isolation */
}
/* Variant: matching very dark text to dark background */
.consent-text {
color: #0a0a0a; /* even darker than background */
mix-blend-mode: darken;
/* min(0.039, 0.051) = 0.039 → rgb(10,10,10) on #0d0d0d → CR = 1.01:1 → invisible */
}
/* Attackers may use CSS custom properties to match the background:
:root { --bg: #0d0d0d; }
.consent-text { color: var(--bg); mix-blend-mode: darken; }
The consent text color is DEFINED as the background color. */
/* Detection:
const cs = getComputedStyle(el);
if (cs.mixBlendMode === 'darken') {
const fgColor = parseRgb(cs.color);
const bgColor = getEffectiveBackground(el);
const blended = {
r: Math.min(fgColor.r, bgColor.r),
g: Math.min(fgColor.g, bgColor.g),
b: Math.min(fgColor.b, bgColor.b)
};
const contrast = contrastRatio(blended, bgColor);
if (contrast < 1.5) → CRITICAL (SA-CSS-MBD-003)
} */
darken is the natural complement of lighten for dark-theme attacks: The existing SA-CSS-MBM-001 (lighten) attack works on white backgrounds: max(dark, white) = white. The SA-CSS-MBD-003 (darken) attack is its mirror: it works on dark backgrounds by selecting the minimum of two dark values. Together, lighten and darken cover both light and dark themes with a single-property attack.
mix-blend-mode:darken + dark text on dark background: min(Cs,Cb) selects the darker of two near-identical dark values; output ≈ background. Detection: blend formula output contrast ratio < 1.5 → CRITICAL.SA-CSS-MBD-004: mix-blend-mode:exclusion — near-identical color pairs
The exclusion blend mode formula is Cs + Cb − 2 × Cs × Cb — similar to difference but with softer transitions. At matched colors (Cs = Cb = x): x + x − 2x² = 2x(1−x), which equals zero at x=0 or x=1 and peaks at 0.5 for x=0.5. For dark colors (x ≈ 0.05–0.15), the output is near-zero: almost black. On a dark background, near-zero output is invisible. The attack exploits this: apply exclusion on consent text whose color is close (but not identical) to a dark background to produce black-on-dark output.
/* Attack: exclusion on matched low-luminance colors → near-black output */
.consent-wrapper {
background: #111111; /* dark: Cb ≈ 0.067 per channel */
}
.consent-text {
color: #111111; /* matched color: Cs = Cb ≈ 0.067 */
mix-blend-mode: exclusion;
/* exclusion(0.067, 0.067) = 0.067+0.067 − 2×0.067×0.067
= 0.134 − 0.009 = 0.125
rgb(32,32,32) output on #111111 → CR ≈ 1.1:1 → invisible */
}
/* Evasion variant with slightly mismatched colors */
.consent-text {
color: #0f0f0f; /* Cs ≈ 0.059 vs Cb ≈ 0.067 */
mix-blend-mode: exclusion;
/* exclusion(0.059, 0.067) = 0.059+0.067 − 2×0.059×0.067
= 0.126 − 0.008 = 0.118 → rgb(30,30,30) on #111111 → CR ≈ 1.09:1 */
}
/* Note: exclusion ≠ difference — the output is not exactly |Cs-Cb|.
Exclusion is 'softer' near matched values: produces non-zero output
even at matched colors (unlike difference which produces exactly 0).
Detection cannot rely on exact-zero checks; must evaluate contrast ratio. */
/* Detection:
const cs = getComputedStyle(el);
if (cs.mixBlendMode === 'exclusion') {
const fg = parseRgb(cs.color);
const bg = getEffectiveBackground(el);
const blended = {
r: fg.r + bg.r - 2 * fg.r * bg.r,
g: fg.g + bg.g - 2 * fg.g * bg.g,
b: fg.b + bg.b - 2 * fg.b * bg.b
};
const contrast = contrastRatio(blended, bg);
if (contrast < 2.0) → HIGH (SA-CSS-MBD-004)
// 2.0 threshold (not 1.5) because exclusion formula is softer near matched values
} */
Universal blend mode detector pattern: Rather than hard-coding each blend mode formula, implement a generic blend detector. For any mixBlendMode value: (1) compute the blended output using the W3C compositing formula for that mode, (2) compare the blended output luminance against the background luminance using the WCAG contrast ratio formula, (3) flag any result below 2.0:1 as HIGH and below 1.5:1 as CRITICAL. This single function covers all 16 non-normal blend modes without a dedicated check for each.
mix-blend-mode:exclusion + near-matched dark foreground/background: Cs+Cb−2CsCb produces near-black on dark background. Detection: blend formula output contrast ratio < 2.0 with exclusion → HIGH.Summary of SA-CSS-MBD findings
| ID | Severity | Blend mode | Attack scenario | Formula output | Detection |
|---|---|---|---|---|---|
| SA-CSS-MBD-001 | CRITICAL | multiply | White/near-white text on white background | Cs×Cb ≈ 1.0 → white-on-white | Contrast of blended output < 1.5 |
| SA-CSS-MBD-002 | CRITICAL | difference | Matched or near-matched fg/bg color on dark theme | |Cs−Cb| ≈ 0 → black-on-dark | Contrast of blended output < 1.5 |
| SA-CSS-MBD-003 | CRITICAL | darken | Dark text on dark background | min(Cs,Cb) ≈ Cb → dark-on-dark | Contrast of blended output < 1.5 |
| SA-CSS-MBD-004 | HIGH | exclusion | Near-matched low-luminance colors | Cs+Cb−2CsCb ≈ near-black on dark bg | Contrast of blended output < 2.0 |
Related pages
- SA-CSS-MBM: CSS mix-blend-mode white background consent attacks (lighten, screen, color-dodge, soft-light)
- SA-CSS-BDF: CSS backdrop-filter consent attacks
- SA-CSS-FSZ: CSS font-size:0 consent attacks
- SA-CSS-CFO: CSS color format obfuscation (HSL, OKLCH, Lab, display-p3)
- All SkillAudit security research