MCP server CSS color:transparent consent security
Four SA-CSS-CFA consent attack patterns exploiting the CSS color property for text visibility attacks that bypass opacity-based detectors. color: transparent renders text invisible via transparent foreground paint while leaving the element fully opaque (opacity:1), selectable, and screen-reader accessible. color: rgba(17,17,17,0.03) achieves near-identical visual invisibility but reports as a legitimate dark color with alpha channel — not obviously an attack. text-shadow combined with transparent text color produces a low-contrast shadow-only rendering that evades both opacity and color checks. Pseudo-element overlays can apply white-colored content directly over consent text without modifying the text element itself.
The CSS color property is the primary mechanism for text visibility in browsers. A security detector that monitors opacity misses color-based attacks entirely — the element remains fully opaque while its text is painted transparent. Unlike opacity, which is inherited and applies to the entire subtree, color is inherited but can be set on specific text elements without affecting siblings or the dialog container. This makes color-based consent attacks more surgical and harder to detect with container-level scanning.
SA-CSS-CFA-001: color:transparent — invisible text, full layout footprint
Setting color: transparent makes text glyphs invisible: the foreground paint color is transparent, so no ink is applied to the canvas. The text still occupies its full layout space (line height, width, line breaks), remains selectable with Ctrl+A / click-drag, and is announced by screen readers. The element reports opacity:1. Only examining getComputedStyle(el).color reveals the attack: the computed value is rgba(0,0,0,0).
.consent-text {
color: transparent; /* CSS keyword: equivalent to rgba(0,0,0,0) */
}
/* Computed properties:
getComputedStyle(el).color → "rgba(0, 0, 0, 0)"
getComputedStyle(el).opacity → "1" (not modified)
el.style.opacity → "" (not in inline style)
el.getBoundingClientRect().height → full height (layout intact)
el.textContent → consent text (readable via DOM)
WCAG contrast: text color rgba(0,0,0,0) over white background:
Effective text luminance = fully transparent → blended with background white.
blend_L = 0 × 0 + 1.0 × 1.0 = 1.0 (fully white-blended)
Contrast: (1.05) / (1.0 + 0.05) = 1.0:1 — background vs background = NO contrast
Screen reader: text IS accessible (color:transparent does not affect a11y tree).
Intent: human users cannot read; automated/a11y tooling can still find the text.
This is sometimes cited as "the text is accessible" — but human users are the ones
granting consent, not screen readers.
Evades:
- opacity checks (opacity:1)
- visibility checks (visibility:visible)
- display checks (display:block)
Detection:
Parse getComputedStyle(el).color for consent text elements.
Extract alpha channel from rgba() value.
If alpha < 0.15 → CRITICAL SA-CSS-CFA-001.
Also match CSS keyword "transparent" directly. */
color:transparent is a common legitimate pattern — used for text-shadow-only labels, icon labels for accessibility, and placeholder animations. Audit context matters: flag color:transparent specifically on elements inside consent dialogs (role=dialog, class patterns containing consent, permission, confirm, allow). Outside consent context it may be benign.
SA-CSS-CFA-002: color:rgba(17,17,17,0.03) — near-zero alpha dark color
Rather than the transparent keyword, a skill author can specify a near-zero alpha color that appears to be a standard dark text color. rgba(17,17,17,0.03) looks like dark charcoal text at 3% opacity — not an obvious attack. The computed style returns a color, not "transparent". Visual result: consent text at WCAG contrast ratio 1.03:1 — effectively invisible on a white background.
.consent-text {
color: rgba(17, 17, 17, 0.03); /* appears to be normal dark color with slight alpha */
}
/* WCAG contrast calculation:
Text color: rgba(17,17,17,0.03) on white background:
R=17, G=17, B=17 → sRGB = (17/255) = 0.0667 → linearized ≈ 0.0045 each channel
Relative luminance of #111111: L = 0.2126×0.0045 + 0.7152×0.0045 + 0.0722×0.0045
= 0.0045 (very dark)
But with alpha=0.03, the effective color blended over white:
blend_L = 0.03 × 0.0045 + 0.97 × 1.0 ≈ 0.970
Contrast: 1.05 / (0.970 + 0.05) ≈ 1.03:1 — FAIL (needs 4.5:1 for body text)
getComputedStyle(el).color → "rgba(17, 17, 17, 0.03)" — not "transparent"
getComputedStyle(el).opacity → "1"
Evasion variants:
rgba(0, 0, 128, 0.04) — dark blue, alpha=0.04 → WCAG ≈ 1.03:1
rgba(139, 0, 0, 0.05) — dark red, alpha=0.05 → WCAG ≈ 1.04:1
rgba(17, 17, 17, 0.1) — alpha=0.1 → blend_L ≈ 0.900 → WCAG ≈ 1.15:1 (still FAIL)
rgba(17, 17, 17, 0.15) — alpha=0.15 → blend_L ≈ 0.851 → WCAG ≈ 1.22:1 (FAIL)
(WCAG requires ≥4.5:1 for normal text, ≥3:1 for large text)
Detection:
Parse color CSS for all consent-text elements.
Extract RGBA alpha channel.
Compute WCAG contrast ratio using blended luminance formula.
If contrast ratio < 4.5:1 (normal text) or < 3:1 (large text) → CRITICAL.
This catches both color:transparent AND color:rgba(x,y,z,low-alpha) patterns. */
SA-CSS-CFA-003: color:transparent + text-shadow — shadow-only glyph rendering
Combining color: transparent with text-shadow produces a rendering where the glyph strokes are painted only by the shadow, not by the foreground color. The shadow renders slightly offset and blurred, producing a smear approximating the glyph shape at the shadow color opacity. This creates a faint visual impression of text that is insufficient for reading but may fool a user into believing the text is visible. The effective WCAG contrast is much lower than a same-opacity solid foreground color because shadow blur distributes ink away from glyph centers.
.consent-text {
color: transparent;
text-shadow: 0 0 0 rgba(17, 17, 17, 0.3); /* zero-offset shadow at 30% opacity */
}
/* Effective rendering:
Foreground: transparent (no ink from text glyphs)
Shadow: rgba(17,17,17,0.3) at 0px offset, 0px blur
At 0 offset, 0 blur: shadow renders exactly coincident with glyph edges.
Ink concentration ≈ shadow alpha × glyph coverage (varies by typeface).
WCAG approximation for 0-blur shadow:
Effective color ≈ rgba(17,17,17,0.3) on white background
blend_L = 0.3 × 0.0045 + 0.7 × 1.0 = 0.001 + 0.7 = 0.701
Contrast: 1.05 / (0.701 + 0.05) ≈ 1.40:1 — FAIL
Layered shadows (multiple): can stack alpha
text-shadow:
0 0 0 rgba(17,17,17,0.15),
0 0 0 rgba(17,17,17,0.15),
0 0 0 rgba(17,17,17,0.15);
Stacked alpha (Porter-Duff over): A_stacked = 1-(1-0.15)^3 = 1-0.614 = 0.386
Effective color ≈ rgba(17,17,17,0.386) → blend_L ≈ 0.634 → contrast ≈ 1.55:1 — FAIL
Blurred shadow (4px blur): ink smears across glyph area and beyond.
Effective peak ink density ≈ 0.3 / blur_spread_factor ≈ 0.3 / 3 ≈ 0.1 (rough estimate)
At 0.1 effective density: blend_L ≈ 0.900 → contrast ≈ 1.15:1 — FAIL
More visible variant: text-shadow only with solid color but very low explicit spread
text-shadow: 0 0 2px rgba(17,17,17,0.8); /* blurred dark shadow */
Effective peak contrast at glyph edges: depends on render engine, DPI, and font.
Generally <3:1 for 2px blur even at high shadow alpha.
Detection:
If color:transparent + any text-shadow present on consent element:
→ Flag as HIGH SA-CSS-CFA-003 (shadow provides insufficient contrast).
Compute effective WCAG via shadow: shadow_alpha × 0.0045 (dark) as text_L.
If effective contrast < 4.5:1 → CRITICAL. */
Shadow-only rendering mimics visible text: A user may perceive the faint shadow glyph smear as "slightly faded" text rather than unreadable text. This creates a social-engineering angle: consent text appears visible but is effectively unreadable, and users may accept it rather than questioning why text looks "blurry."
SA-CSS-CFA-004: pseudo-element overlay with white or background-matching color
A ::before or ::after pseudo-element on the consent text container can be given color: white (or the background color), content: attr(data-text) to clone the consent text, and position: absolute to overlay exactly on top of the original text. The original text remains at full opacity and dark color — but is visually covered by a white-colored pseudo-element clone. This attack is virtually undetectable by opacity and color checks on the consent text element itself.
.consent-text {
position: relative;
color: #111111; /* original text: fully legible color */
opacity: 1; /* no opacity attack */
}
/* Pseudo-element overlay covers the text with white */
.consent-text::after {
content: '';
position: absolute;
inset: 0; /* covers entire consent-text element */
background-color: white; /* or background-color: var(--dialog-bg) */
z-index: 1; /* renders above consent text */
}
/* Variant: clone text content via attr() */
.consent-text[data-consent]::before {
content: attr(data-consent); /* renders a copy of the consent text */
position: absolute;
top: 0; left: 0;
color: white; /* white text over white background = invisible */
/* The cloned white text covers the original dark text */
}
/* More surgical: target only the text span, not the button */
.consent-dialog .consent-message::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 100%;
background: rgba(255,255,255,0.97); /* near-opaque white overlay */
}
/* Detector evasion: the consent-text element itself passes all checks:
getComputedStyle(el).color → "rgb(17, 17, 17)" (dark, good)
getComputedStyle(el).opacity → "1" (good)
el.getBoundingClientRect() → normal dimensions (good)
But the user cannot see the text because the pseudo-element is on top.
Detection:
Check ::before and ::after pseudo-elements of consent text and its ancestors.
If any pseudo-element has: position:absolute/fixed + coverage area ≥ 80% of
consent text bounding box + background-color opacity ≥ 0.8: → CRITICAL.
Also check computed z-index ordering within consent dialog subtree. */
Summary of SA-CSS-CFA findings
| ID | Severity | Pattern | Mechanism | Evasion |
|---|---|---|---|---|
| SA-CSS-CFA-001 | CRITICAL | color:transparent | Fully transparent foreground; opacity:1; layout intact; WCAG 1.0:1 | Evades opacity checks |
| SA-CSS-CFA-002 | CRITICAL | color:rgba(17,17,17,0.03) | Near-zero alpha dark color; appears normal; WCAG 1.03:1 | Evades opacity + transparent keyword checks |
| SA-CSS-CFA-003 | HIGH | color:transparent + text-shadow | Shadow-only glyph smear; WCAG ≈1.40:1; mimics visible text | Evades color checks; visual mimicry |
| SA-CSS-CFA-004 | CRITICAL | ::after white overlay | Pseudo-element covers text; consent element passes all property checks | Evades element-level property checks entirely |
Related pages
- CSS text-shadow and -webkit-text-stroke consent contrast attacks (SA-CSS-TXS)
- CSS @keyframes percentage stops consent attacks (SA-CSS-WKF)
- CSS animation-timing-function cubic-bezier extreme easing consent attacks (SA-CSS-ATF)
- CSS animation-iteration-count consent attacks (SA-CSS-AIC)
- MCP server prompt injection security
- All SkillAudit security research