Security Guide
MCP server CSS color-contrast() security — adversarial minimum-contrast color selection for consent text
The CSS color-contrast() function selects the color from a provided list that best meets a contrast requirement against a given background. It was designed to automate accessible color choices. An MCP server inverts this intent: by populating the list with colors that all hover just above the WCAG AA threshold, it reliably selects the hardest-to-read compliant color for consent text — passing every automated accessibility audit while making consent maximally difficult to read for human users.
How color-contrast() works
The color-contrast() function (CSS Color Level 6) takes a background color, a keyword, and a list of foreground color candidates. It evaluates each candidate's contrast ratio against the background and returns the one that best satisfies the specified threshold. Browser support: Safari Technology Preview (experimental), Chrome Origin Trial — approximately 10-15% of browsers in 2026. However, the function is actively being standardized and is expected to gain broad support.
/* Syntax */ color-contrast(background-color vs color-list [to contrast-ratio|wcag2(aa)|wcag2(aaa)]) /* Example: picks the highest-contrast color from the list */ color: color-contrast(white vs black, blue, red); /* → black (highest contrast against white = 21:1) */ /* With explicit threshold: picks the first color that meets the threshold */ color: color-contrast(white vs lightgray, gray, darkgray to wcag2(aa)); /* → first color meeting 4.5:1 against white */ /* If no color in the list meets the threshold, the function returns the color with the highest contrast (closest to meeting the requirement) */
Implementation note: The color-contrast() function's exact fallback behavior when no color meets the threshold varies between browsers and specification drafts. This creates additional attack surface: in some implementations, specifying a threshold that none of the listed colors meet causes the function to return the color with the highest contrast — which an attacker can exploit by supplying a threshold higher than all candidates, forcing the "best available" selection from a list of bad options.
Attack 1 (HIGH): Adversarial list of colors all just above WCAG AA threshold
The MCP server constructs a list of colors each with a contrast ratio of approximately 4.51:1 to 4.99:1 against the consent dialog's background color. Every color in the list technically passes WCAG AA (which requires 4.5:1 for normal text). The color-contrast() function, finding all colors compliant, may return any of them — but since they are all at the minimum end of the pass range, the resulting text color is always the hardest shade that still passes. No automated WCAG audit will flag the result because the contrast ratio is ≥ 4.5:1. Human readers, however, experience text that is noticeably harder to read than standard black-on-white (21:1).
/* All candidate colors have contrast ~4.5-4.7:1 against white background */
/* These are the minimum-passing shades — technically WCAG AA compliant */
.consent-text {
background: white; /* #ffffff */
color: color-contrast(
white vs
/* Each color below has contrast ratio ~4.5-4.7:1 against white */
#767676, /* contrast 4.54:1 — barely passes AA */
#757575, /* contrast 4.60:1 — barely passes AA */
#747474, /* contrast 4.66:1 — barely passes AA */
#737373, /* contrast 4.72:1 — barely passes AA */
#727272 /* contrast 4.78:1 — barely passes AA */
to wcag2(aa)
);
/* Result: first color meeting wcag2(aa) = #767676 (lowest contrast that passes)
Automated WCAG checker: PASS (4.54:1 ≥ 4.5:1)
Human experience: very low contrast gray text on white — noticeably hard to read
Standard black text would give 21:1 contrast.
This is 4.6× worse than achievable, while still technically compliant.
*/
}
Attack 2 (CRITICAL): color-contrast() with custom property background — theme-adaptive attack
Using a CSS custom property as the background color input, the attack adapts to any page theme. The MCP server pre-computes a set of colors that all just pass 4.5:1 against various common background shades (white #fff, off-white #f8f8f8, light gray #f0f0f0, dark mode near-black #1a1a1a) and supplies them all in the list. The color-contrast() function then dynamically selects the worst-passing color for whatever background the user's device happens to use — including dark mode backgrounds. The attack is theme-neutral: it degrades consent readability on any color scheme without any conditional CSS.
/* Theme-adaptive adversarial contrast — works on both light and dark mode */
:root {
--page-bg: #ffffff; /* or #1a1a1a in dark mode */
}
.consent-text {
background: var(--page-bg);
color: color-contrast(
var(--page-bg) vs
/* Colors pre-computed to barely pass against both #fff and #1a1a1a */
#767676, /* 4.54:1 on white; 4.62:1 on #1a1a1a */
#8a8a8a, /* 3.95:1 on white (fails); but 6.1:1 on #1a1a1a */
#909090, /* 3.71:1 on white (fails); 6.6:1 on #1a1a1a — too much contrast */
#777777, /* 4.48:1 on white (barely fails) */
#787878 /* 4.54:1 on white — barely passes */
to wcag2(aa)
);
/* On white: first passing color = #767676 (4.54:1) — minimum contrast
On dark #1a1a1a: first passing color = #767676 (4.62:1) — minimum contrast
Both cases: text is as hard to read as technically possible.
*/
}
Attack 3: color-contrast() used for background color — consent container fades into page
The attack is not limited to text color. An MCP server can use color-contrast() to select the consent container's background color from a list of near-background colors that all technically provide sufficient contrast against the text, but make the consent dialog appear to blend into the page. The background is selected to be as close to the page background as possible while still providing 4.5:1 contrast against the text. This reduces the visual salience of the consent dialog — users notice it less, are less likely to read it carefully, and may click through without conscious engagement.
/* Adversarial consent container background — blends into page */
body {
background: #f8f8f8; /* page background */
}
.consent-dialog {
/* Picks the color from the list that most closely resembles the page background
while still providing 4.5:1 contrast against the text color #222 */
background: color-contrast(
#222 vs /* consent text color */
#f8f8f8, /* same as page background — would be invisible, 15.3:1 against #222 */
#f6f6f6, /* 14.9:1 against #222 — still barely distinguishable from page */
#f0f0f0, /* 13.4:1 against #222 — slightly more visible */
#e8e8e8 /* 11.6:1 against #222 — more visible still */
to wcag2(aa) /* requires 4.5:1 contrast — all candidates far exceed this */
);
/* Result: #f8f8f8 — the consent dialog background is IDENTICAL to the page background.
The dialog appears to have no visual boundary from the rest of the page.
The contrast check is against the text, not against the page — so it passes WCAG.
*/
}
Attack 4: Fallback exploitation when color-contrast() is unsupported
Because color-contrast() is not universally supported, browsers that do not support it ignore the declaration and fall through to the preceding valid color declaration. An MCP server that targets only browsers supporting color-contrast() can pair a safe fallback (color: #1a1a1a) with an adversarial color-contrast() override. On browsers that support the function (Safari TP, Chrome with origin trial), the minimum-contrast color is applied. On browsers that do not (Firefox, older Chrome), the safe fallback is used. This ensures that security researchers using a non-supporting browser see the safe color, while target users on compliant browsers see the minimum-contrast color.
/* Browser-targeted attack via color-contrast() fallback */
.consent-text {
background: white;
/* Fallback: safe readable color — used by Firefox, older Chrome */
color: #1a1a1a; /* 16.1:1 contrast — clearly readable */
/* Override: adversarial minimum-contrast — used by browsers supporting color-contrast() */
color: color-contrast(
white vs #767676, #757575, #747474
to wcag2(aa)
); /* → #767676 — 4.54:1, barely passing */
}
/* Security researcher on Firefox: sees color:#1a1a1a → reports consent is readable.
User on Safari TP / Chrome with origin trial: sees color:#767676 → barely readable.
WCAG audit on supporting browser: reports 4.54:1 → PASS (threshold is 4.5:1).
*/
Detection implementation
/**
* SkillAudit: detect adversarial color-contrast() usage on consent elements
*/
function detectColorContrastAttacks(consentSelector = '[data-consent], .consent, #consent-dialog') {
const findings = [];
const COLOR_CONTRAST_PATTERN = /color-contrast\s*\(/i;
// Scan stylesheets for color-contrast() usage
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.STYLE_RULE) continue;
for (const prop of ['color', 'background', 'background-color']) {
const val = rule.style.getPropertyValue(prop);
if (!val || !COLOR_CONTRAST_PATTERN.test(val)) continue;
// Heuristic: count the number of candidate colors
// An adversarial list typically has 3-7 closely-spaced colors
const colorCount = (val.match(/,/g) || []).length;
findings.push({
severity: 'HIGH',
selector: rule.selectorText,
property: prop,
value: val,
detail: `${prop} uses color-contrast() with ${colorCount} candidate color(s). Verify the candidate list is not populated with colors that all barely pass WCAG AA — adversarial use picks the minimum-readable compliant color.`,
});
}
}
}
// Check computed colors on consent elements
const consentEls = document.querySelectorAll(consentSelector);
for (const el of consentEls) {
const cs = getComputedStyle(el);
const color = cs.color;
const bg = cs.backgroundColor;
if (!color || !bg) continue;
// Parse RGB values and compute approximate contrast ratio
function parseRgb(str) {
const m = str.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
return m ? [+m[1], +m[2], +m[3]] : null;
}
function relativeLum(r, g, b) {
const toLinear = v => { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
const fgRgb = parseRgb(color);
const bgRgb = parseRgb(bg);
if (!fgRgb || !bgRgb) continue;
const L1 = relativeLum(...fgRgb);
const L2 = relativeLum(...bgRgb);
const contrast = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
// Flag if contrast is in the "barely passing" range: 4.5:1 to 5.5:1
if (contrast >= 4.5 && contrast < 5.5) {
findings.push({
severity: 'WARN',
element: el,
contrast: contrast.toFixed(2),
detail: `Consent element has computed contrast ${contrast.toFixed(2)}:1 — in the barely-passing WCAG AA range. If set via color-contrast(), this may indicate adversarial minimum-contrast selection.`,
});
}
}
return findings;
}
| Attack | Mechanism | Audit bypass |
|---|---|---|
| Adversarial color list — all just above 4.5:1 | color-contrast() picks worst-passing color | Automated WCAG check passes (≥4.5:1) — human readability degraded |
| Custom property background — theme adaptive | Pre-computed minimum-passing colors for any background | Works in light and dark mode; no conditional CSS needed |
| Background color — dialog blends into page | color-contrast() selects near-page-background | WCAG checks contrast against text (not page BG) — passes; visual salience reduced |
| Fallback targeting supporting browsers only | Safe color on non-supporting; adversarial on supporting | Researcher on Firefox sees safe color; users on Safari TP / Chrome OT see adversarial |
Related SkillAudit coverage
- CSS color() function and wide-gamut color space attacks on consent readability
- CSS color-mix() function attacks blending consent text toward background
- CSS relative color syntax adjusting consent text contrast via channel manipulation
- CSS light-dark() function — theme-adaptive consent color attacks
- CSS device-cmyk() — device-specific color conversion hiding consent text
SkillAudit detection: SkillAudit scans all stylesheets for color-contrast() usage on selectors affecting consent elements. For each occurrence, it parses the candidate color list, evaluates each candidate's contrast ratio against the specified background, and flags cases where the entire list clusters near the WCAG AA threshold rather than providing a genuine choice between accessible and inaccessible colors. The computed contrast of consent element text colors in the 4.5–5.5:1 range is also flagged as a potential adversarial minimum-contrast selection.
Audit your MCP server's color selection strategy for adversarial minimum-contrast patterns. Run a free SkillAudit scan — results in 60 seconds.