CSS Text Property Consent Attacks: color:transparent, Near-Zero Alpha, text-shadow White Glow, and -webkit-text-stroke Overlay
Eight attack patterns across two CSS property families — color alpha channel manipulation and text-shadow/-webkit-text-stroke rendering interference — that reduce MCP server consent text legibility to below WCAG 1.4.3 thresholds. All eight preserve opacity:1. All eight return apparently valid computed color values from getComputedStyle(el).color or leave that value unchanged. The unifying theme: audit engines that check the CSS opacity property and the color keyword are reading the wrong signal — they must compute the effective pixel luminance of consent text after compositing all applicable rendering properties.
The opacity-blind auditor problem
Most first-generation MCP consent auditors check a short list of CSS properties: opacity, display, visibility, and perhaps color against a list of known transparent values. These checks are necessary but far from sufficient. The eight patterns in this post all evade them completely.
The fundamental failure is treating individual CSS properties as independent. In reality, the visible luminance of a rendered text pixel is determined by the composition of multiple properties operating simultaneously: the foreground color (including its alpha channel), the background color of the element and all its ancestors, any text-shadow values applied (which paint additional pixels both behind and around the glyph shapes), -webkit-text-stroke (which paints a stroke that can cover glyph interiors), and pseudo-element overlays (::before, ::after) that may paint over the text after it has been rendered.
Consider the detection gap for opacity specifically. The CSS opacity property applies a uniform alpha multiplier to the entire element, including its background, children, and text. Setting opacity:0.1 is detectable with a trivial property check. But the CSS color property carries its own alpha channel in the rgba() notation. color:rgba(17,17,17,0.03) makes the foreground color approximately 97% transparent, resulting in WCAG contrast of 1.03:1 — effectively invisible — while getComputedStyle(el).opacity returns “1” and no opacity alarm fires. The color property’s alpha channel is invisible to opacity-checking auditors.
Similarly, text-shadow and -webkit-text-stroke do not change the color property value at all. They operate at the rendering layer, painting additional pixels that mix with or cover the glyph pixels after CSS property values are resolved. A tool that reads getComputedStyle(el).color and finds rgb(17, 17, 17) — which implies 19.6:1 contrast on a white background — has completely missed the fact that a white glow text-shadow or a white -webkit-text-stroke has reduced the effective luminance of every pixel on every glyph stroke to a value much closer to the background. The only reliable signal is the actual rendered pixel luminance of the consent text, which requires a compositing-aware analysis of all applicable properties simultaneously.
SA-CSS-CFA: color alpha channel attacks
The SA-CSS-CFA series covers four patterns in which the CSS color property itself is the attack vector. In CSS, the foreground color of text can be expressed as a fully opaque hex value (#111111), a named transparent value (transparent), or an rgba() function with an explicit alpha component. The alpha channel in rgba() determines how much of the foreground color is composited onto the background when the glyph is rendered. At alpha=0, the foreground contributes nothing; the background shows through completely. At alpha=0.03, the contribution is so small that the effective WCAG contrast ratio is 1.03:1, which is below the 1:1 minimum (indistinguishable from the background). All four patterns set opacity:1 on the element and achieve the attack entirely through the color property’s own alpha channel or via mechanisms layered on top of a transparent foreground.
CFA-001: color:transparent
The CSS keyword transparent is equivalent to rgba(0,0,0,0) — a fully transparent foreground color. The element’s layout box is preserved, its dimensions are unchanged, it occupies space in the document flow, and it is accessible to screen readers (the text is in the DOM and in the accessibility tree). getComputedStyle(el).opacity returns “1”. The element is visually invisible: the glyph shapes are present in the rendering pipeline but the foreground color composites with alpha=0, contributing zero luminance delta against the background. On a white background, every pixel of the text renders as white (background showing through).
This differs critically from opacity:0: both make the text invisible, but they are detectable by different checks. opacity:0 is caught by getComputedStyle(el).opacity === "0". color:transparent is not caught by an opacity check — it requires parsing the color property’s rgba value and checking whether the alpha component is below a legibility threshold.
CFA-002: color:rgba(17,17,17,0.03) — near-zero alpha
Rather than using the transparent keyword (which some auditors may detect via string match), an attacker can use rgba() with an alpha value just above zero. At alpha=0.03, the effective WCAG contrast is computed as follows. The foreground color is dark (#111, linear luminance ≈0.003). The background is white (linear luminance=1.0). The effective luminance of a glyph pixel after alpha compositing is: L_effective = alpha × L_foreground + (1−alpha) × L_background = 0.03×0.003 + 0.97×1.0 ≈ 0.970. The WCAG contrast ratio is: (1.0 + 0.05) / (0.970 + 0.05) = 1.05 / 1.020 ≈ 1.03:1. This is below WCAG Level AA (4.5:1 for normal text) and barely above the theoretical minimum of 1:1. The text is nearly completely invisible on a white background.
The attack evades the transparent keyword check because getComputedStyle(el).color returns “rgba(17, 17, 17, 0.03)” — a syntactically valid dark color, not the word “transparent.” An auditor checking color !== "transparent" passes this. Detection requires parsing the rgba alpha channel and computing effective contrast, not just checking the color name.
CFA-003: color:transparent + dark text-shadow
With the foreground color set to transparent, the only visual information about glyph shapes comes from any applied text-shadow. A dark shadow at rgba(17,17,17,0.3) at 0–1px offset renders behind the (transparent) foreground, providing a low-contrast smear of the glyph outlines. Because the shadow is rendered behind the fully transparent foreground color, the shadow is only visible at positions where the glyph’s anti-aliased edge pixels have sub-1.0 alpha — the interior glyph pixels, which have full alpha, are covered by the transparent foreground (which composites as fully transparent, showing background white at those positions, which covers the shadow). The net effect: glyph shapes appear as a faint outline smear, approximately 1.40:1 WCAG contrast. This mimics disabled or placeholder text styling and may be misread as a UI state rather than an attack.
CFA-004: ::after pseudo-element white background overlay
This is the most evasive pattern in the SA-CSS-CFA series. The consent text element itself has a fully valid, high-contrast color (#111111). The getComputedStyle(el).opacity, getComputedStyle(el).color, getComputedStyle(el).visibility, and getComputedStyle(el).display all return normal values. Every property check on the element itself passes. The attack is in a ::after pseudo-element on the consent text’s ancestor, which positions a white rectangle covering the consent text:
.consent-container::after {
content: '';
position: absolute;
inset: 0; /* covers entire container */
background-color: white; /* white rectangle over all content */
z-index: 1; /* above consent text in stacking order */
}
/* Detection requires:
const afterStyle = getComputedStyle(consentTextEl.parentElement, '::after');
if (afterStyle.content !== 'none'
&& afterStyle.position === 'absolute'
&& parseFloat(afterStyle.opacity) > 0.5
&& isWhiteOrNearWhite(afterStyle.backgroundColor)) {
→ CRITICAL SA-CSS-CFA-004
}
The attack can appear on ::before, ::after of the element itself or any ancestor.
Full detection requires checking ::before and ::after on the element and
all ancestor elements up to the consent dialog boundary. */
Detection of CFA-004 requires retrieving pseudo-element computed styles via the second argument of getComputedStyle(el, '::after') and checking whether the pseudo-element’s background color, position, and z-index would result in coverage of the consent text. This is a fundamentally different detection path from all other SA-CSS-CFA patterns, which examine properties of the consent text element directly. See the SA-CSS-CFA detail page for the full four-pattern analysis and detection code.
SA-CSS-TXS: text rendering interference attacks
The SA-CSS-TXS series covers four patterns in which the CSS color property appears correct, but post-color rendering operations degrade the effective legibility of consent text below WCAG thresholds. In all four patterns, getComputedStyle(el).opacity returns “1” and getComputedStyle(el).color returns an apparently valid foreground color. The attacks operate at the pixel compositing layer, after CSS property values are resolved.
TXS-001: text-shadow:0 0 8px white — Gaussian glow over dark glyphs
The CSS text-shadow property with a white color and an 8px blur radius renders a white Gaussian glow around each glyph. The shadow is painted behind the foreground text, but the Gaussian blur spreads white pixels into the areas adjacent to glyph strokes. For a 16px font with 1.5px average stroke width, an 8px white blur has a non-negligible white contribution at the center of each stroke pixel, reducing the effective local contrast. The font-weight dependency is critical: bold text (weight:700, stroke ≈4px) has enough ink mass to absorb the glow without significant contrast reduction. Light text (weight:300, stroke ≈0.7px) has narrow strokes that are nearly completely washed out by an 8px white blur — effective WCAG contrast drops to 2–3:1.
The attack is particularly effective because MCP consent dialog body text is commonly set at normal to light weight (300–400) in modern design systems. An auditor checking contrast on a bold element may pass the check while the actual consent text in a lighter weight fails.
TXS-002: stacked rgba(255,255,255,0.25) shadows — additive white overlay
Rather than using a high-alpha single white shadow (which would be straightforward to detect), this pattern stacks four text-shadow values, each at rgba(255,255,255,0.25). Each individual shadow has only 25% white alpha — superficially resembling a legitimate “soft glow” UI effect. But shadows composite additively via the Porter-Duff over operation. The combined white alpha at any glyph pixel is: A_combined = 1−(1−0.25)^4 = 1−0.316 = 0.684. This 68.4% white overlay on a dark glyph produces an effective glyph pixel luminance of: L_glyph = 0.684×1.0 + 0.316×0.067 ≈ 0.705. The WCAG contrast against a white background is: 1.05 / (0.456 + 0.05) ≈ 2.08:1 (using L_sRGB ≈ 0.705^2 ≈ 0.497, then adjusted ≈ 0.456). This is a WCAG Level AA failure for normal text (required: 4.5:1).
The attack evades detection because each individual shadow value looks benign. Four shadows at 0.25 alpha each is a common pattern in design systems for adding visual depth. The composite effect is only revealed by computing the aggregate white alpha across all shadow layers and then computing the resulting WCAG contrast.
TXS-003: -webkit-text-stroke:2px white — stroke covers glyph interior
The -webkit-text-stroke property (supported in all major browsers despite the vendor prefix) applies a stroke around glyph outlines. The stroke width is centered on the glyph edge: half the width is inside the glyph shape, half is outside. A 2px white stroke applies 1px of white inside and 1px outside the glyph boundary.
For a narrow letterform like the vertical bar of i or l at 14px font-weight:400, the natural stroke width is approximately 1.5px. The 2px white stroke covers 1px of that 1.5px interior (two-thirds of the glyph stroke area). The CSS default paint order is stroke over fill (unlike SVG’s paint-order: stroke fill), so the white stroke is painted after the dark fill, covering it. The result: most of the narrow glyph stroke interior is white, and only a thin dark core (approximately 0.5px) remains visible. For thin fonts (weight < 400), the damage is worse: the white stroke can completely cover the glyph stroke interior, leaving no dark pixels in the center of thin strokes. Effective WCAG contrast falls below 3:1 for normal-weight fonts and below 2:1 for thin fonts.
The paint-order CSS property is defined for SVG text elements but has limited support in HTML text rendering contexts. Auditors must not assume paint-order: stroke fill mitigates this attack in HTML consent dialogs. See the SA-CSS-TXS detail page for complete coverage of all four patterns.
TXS-004: color:white + dark shadow — ghost text
Setting the text color to white makes the foreground glyphs invisible on a white background. Adding a dark text-shadow provides the only visible rendering of the glyph shapes. The shadow is painted behind the foreground, so at interior glyph pixels (where foreground alpha is 1.0), the white foreground fully covers the shadow — the shadow is invisible there. At anti-aliased glyph edge pixels (where foreground alpha is 0.3–0.7), the white foreground partially covers the shadow, allowing a fraction of the shadow to show through. The visible shadow alpha at an edge pixel with foreground alpha 0.5 is: visible_shadow_alpha = shadow_alpha × (1 − 0.5) = 0.4 × 0.5 = 0.2. The effective luminance at that edge pixel: L_effective = 0.2×0.003 + 0.8×1.0 ≈ 0.801. WCAG contrast: 1.05 / (0.801 + 0.05) ≈ 1.23:1. The glyph appears as a ghost trace — visible only at anti-aliased edges, with interior strokes completely invisible.
Unified detection function
The common thread across all eight SA-CSS-CFA and SA-CSS-TXS patterns is that no single property check is sufficient. Reliable detection requires computing the effective pixel luminance of consent text after compositing all applicable rendering properties. The following JavaScript function outlines the detection architecture:
function getEffectiveConsentTextLuminance(el) {
const style = getComputedStyle(el);
// Step 1: parse color RGBA
// color may be "transparent", "rgba(r,g,b,a)", "rgb(r,g,b)", or named color.
// transparent = rgba(0,0,0,0). Named colors: look up sRGB triplet.
const color = parseRGBA(style.color);
const bgColor = getEffectiveBackgroundColor(el);
// getEffectiveBackgroundColor: walk ancestors until a non-transparent bg is found.
// Account for opacity of each ancestor in the chain.
// Step 2: apply color alpha compositing
// Effective luminance of a glyph pixel where foreground has alpha color.a:
// L_base = color.a * toLinear(color) + (1 - color.a) * toLinear(bgColor)
const baseL = compositeAlpha(color, bgColor);
// If baseL is near toLinear(bgColor): consent text is near-invisible (CFA-001, CFA-002).
// Step 3: check for text-shadow interference
// text-shadow is a comma-separated list of: offset-x offset-y blur color
// White (or near-white) shadows with non-zero alpha reduce effective contrast.
const shadows = parseTextShadows(style.textShadow);
// For each shadow: if color is white/near-white, compute aggregate alpha contribution.
// aggregate_white_alpha = 1 - product_of((1 - shadow_i.alpha) for white shadows)
// If aggregate_white_alpha > 0 and blur > 0: white glow reduces contrast (TXS-001).
// If aggregate_white_alpha > 0 and blur near 0: additive white overlay (TXS-002).
// If color is dark and foreground is white: ghost-text pattern (TXS-004).
const shadowL = computeShadowInterference(shadows, baseL, style.fontWeight);
// Step 4: check -webkit-text-stroke
// -webkit-text-stroke:
// If color is white/near-white and width >= 1px: stroke covers glyph interior.
// Severity scales with font-weight (thin fonts: more coverage).
// Coverage formula (simplified):
// stroke_coverage_fraction = min(1, stroke_width_px / (natural_stroke_width_px))
// natural_stroke_width_px ≈ em_size * 0.07 * (font_weight / 400)
const strokeColor = parseRGBA(style.webkitTextStrokeColor);
const strokeWidth = parseFloat(style.webkitTextStrokeWidth);
const strokeL = computeStrokeCoverage(strokeColor, strokeWidth, style.fontWeight, baseL);
// Step 5: check ::after (and ::before) overlay
// ::after pseudo-element may create a white rectangle covering the consent text.
// Check the element itself and all ancestors up to the consent dialog root.
const afterStyle = getComputedStyle(el, '::after');
if (coversElement(afterStyle, el)) {
// If ::after is opaque and white: effective luminance = white (L=1.0)
return computeEffectiveLuminance(afterStyle.backgroundColor, el);
}
// Also check el.parentElement, grandparent, etc. up to the dialog boundary.
// Return the minimum luminance across all interference mechanisms.
// Lower luminance → lower contrast against white background → more severe attack.
return Math.min(baseL, shadowL, strokeL);
}
// Helper: coversElement checks if ::after visually covers the element.
// Required conditions:
// afterStyle.content !== 'none'
// afterStyle.position === 'absolute' or 'fixed'
// afterStyle.backgroundColor is opaque (alpha >= 0.8) and near-white
// afterStyle.zIndex is positive (above the text flow)
// Bounding box of ::after overlaps bounding box of el
This function represents a detection architecture, not a complete implementation. The helper functions (parseRGBA, getEffectiveBackgroundColor, compositeAlpha, parseTextShadows, computeShadowInterference, computeStrokeCoverage, coversElement) each require careful implementation. getEffectiveBackgroundColor in particular must handle the case where multiple ancestor elements have semi-transparent backgrounds, requiring iterative Porter-Duff compositing up the ancestor chain until an opaque background is found.
The function returns a luminance value. To convert to a WCAG contrast ratio, apply: contrast = (max(L, bgL) + 0.05) / (min(L, bgL) + 0.05). A contrast below 4.5:1 for normal text at the typical sizes used in consent dialogs (13–16px) is a WCAG Level AA failure and a potential consent attack finding. A contrast below 3:1 warrants CRITICAL severity.
Summary table
| ID | Severity | Property | Pattern | Effective contrast |
|---|---|---|---|---|
| SA-CSS-CFA-001 | CRITICAL | color | color:transparent — alpha=0, fully invisible, opacity:1 | 1.00:1 |
| SA-CSS-CFA-002 | CRITICAL | color | color:rgba(17,17,17,0.03) — near-zero alpha, evades keyword check | 1.03:1 |
| SA-CSS-CFA-003 | HIGH | color + text-shadow | color:transparent + dark shadow — shadow-only glyph smear | ≈1.40:1 |
| SA-CSS-CFA-004 | CRITICAL | ::after pseudo-element | ::after white background overlay — all element properties pass; pseudo-element covers text | 1.00:1 (effective) |
| SA-CSS-TXS-001 | HIGH | text-shadow | text-shadow:0 0 8px white — white glow washes thin glyph strokes (weight:300) | 2–3:1 |
| SA-CSS-TXS-002 | CRITICAL | text-shadow | 4× stacked rgba(255,255,255,0.25) — aggregate white alpha ≈0.684 | ≈2.08:1 |
| SA-CSS-TXS-003 | HIGH | -webkit-text-stroke | -webkit-text-stroke:2px white — stroke covers ≥50% of narrow glyph interiors | <3:1 (thin fonts) |
| SA-CSS-TXS-004 | CRITICAL | color + text-shadow | color:white + dark shadow — ghost text at anti-aliased edges only | ≈1.23:1 |
Detection coverage: A single opacity check catches 0 of these 8 patterns. A color:transparent keyword check catches 1 (CFA-001). Parsing the rgba alpha channel catches CFA-001 and CFA-002. Parsing text-shadow values for white/near-white colors catches TXS-001, TXS-002 (requires aggregate alpha computation), and TXS-004. Checking -webkit-text-stroke width and color catches TXS-003. Checking ::after background-color on the element and ancestors catches CFA-004. Checking color:transparent + shadow catches CFA-003. The full set of 8 requires all six detection paths to be active simultaneously.
Related pages
- CSS color:transparent and near-zero alpha consent attacks — SA-CSS-CFA detail page
- CSS text-shadow and -webkit-text-stroke consent attacks — SA-CSS-TXS detail page
- CSS mix-blend-mode consent attacks (SA-CSS-MBM)
- CSS filter brightness/contrast consent attacks (SA-CSS-FBR)
- CSS animation-iteration-count consent attacks (SA-CSS-AIC)
- All SkillAudit security research