← Blog

MCP server CSS text-shadow and -webkit-text-stroke consent security

Four SA-CSS-TXS consent attack patterns using text-shadow and -webkit-text-stroke to reduce consent text contrast below WCAG thresholds. A white 8px glow shadow over dark text spreads white light across glyph strokes, diluting contrast from 19.6:1 to approximately 2.5:1. Layered near-zero shadows create an additive white paint over glyphs. A 2px white stroke applied via -webkit-text-stroke covers narrow glyph interior strokes. Setting text color to white with only a dark shadow produces a ghost-text rendering where glyphs are visible only as a low-contrast shadow smear. All four patterns preserve opacity:1 and a non-transparent color value, evading standard opacity and color checks.

By SkillAudit  |  29 Oct 2026

The CSS text-shadow property paints a copy of the text glyphs at a specified offset, with a blur radius and color, behind the foreground text. When the shadow color is white (or background-matching) and the blur radius is non-zero, the shadow distributes white pixels around and into glyph strokes, reducing the local contrast at each pixel. The -webkit-text-stroke property paints a stroke around each glyph outline; when set to white and wider than the glyph's natural stroke, it erases interior glyph areas. Both mechanisms degrade readability while leaving the color property apparently correct. WCAG 1.4.3 contrast checking tools that only compute foreground-vs-background do not account for text-shadow interference.

SA-CSS-TXS-001: text-shadow white glow — spreading white over dark glyph strokes

Applying a white text-shadow with 8px blur to dark text (#111111) produces a halo of white light around each glyph. The shadow is rendered behind the text but the blur spreads white pixels into the areas adjacent to glyph edges. The effective contrast at each pixel is a composite of the dark text foreground and the white shadow blur at that position. For a 16px font with 1.5px average stroke width, an 8px white blur shadow can reduce effective center-of-stroke contrast from 19.6:1 to approximately 2–3:1.

.consent-text {
  color: #111111;
  text-shadow: 0 0 8px white;   /* 0px offset, 8px blur, solid white */
}

/* How white glow affects contrast:
   Text glyph: dark strokes (#111111) on white background.
   Shadow: white at 0,0 offset with 8px Gaussian blur.
   Shadow renders BELOW foreground text but blur extends into glyph areas.

   At a pixel on a glyph stroke centerline (x=0 relative to stroke center):
   - Foreground text alpha at that pixel: ≈ 0.9 (anti-aliased edge at ~0.5–1.0)
   - Shadow blur contributes white at that position.
   For 8px blur, the Gaussian at stroke center has non-negligible white contribution.
   Simplified: shadow blur effectively "washes" some fraction of each pixel toward white.

   Empirical WCAG impact (8px white text-shadow on 16px body text, white background):
   Text #111111 (L≈0.004) → with shadow: effective L of glyph pixels ≈ 0.05–0.15
   Contrast ratio at stroke edges: (1.05) / (0.15 + 0.05) ≈ 5.25:1 → still passes
   Contrast ratio at stroke interior: (1.05) / (0.05 + 0.05) ≈ 10.5:1 → passes
   But: for thin/light fonts (font-weight:300, stroke ≈ 0.7px):
   Shadow blur at 8px completely overwrites thin strokes → effective L ≈ 0.8
   Contrast: (1.05) / (0.8 + 0.05) ≈ 1.24:1 — FAIL for thin fonts

   Stronger attack: larger blur + higher sigma
   text-shadow: 0 0 20px white, 0 0 20px white;  /* doubled shadow for intensity */
   Two white 20px shadows: additive white contribution to every glyph pixel.
   At stroke width 1px (small consent text, 13px font, light weight): FAIL guaranteed.

   Detection:
   Parse text-shadow values for consent text elements.
   Extract: x-offset, y-offset, blur-radius, color.
   If color = white (rgb(255,255,255) or rgba with A>0.5) + blur-radius > 4px: HIGH.
   If multiple white shadows or blur > 12px: CRITICAL.
   For thin fonts (font-weight < 400), lower thresholds: blur > 2px + white shadow → HIGH. */

Font weight matters: A white glow shadow on bold text (font-weight:700) may not reduce contrast below WCAG thresholds because the thick strokes absorb the shadow blur without losing significant center-of-stroke opacity. The same shadow on font-weight:300 or 400 (typical consent dialog body text) can push center-of-stroke contrast below 3:1. Audit engines must account for font-weight and font-size when evaluating text-shadow severity.

HIGH
SA-CSS-TXS-001 — text-shadow:0 0 8px white on dark text: white glow washes glyph strokes, particularly for thin fonts (font-weight:300–400). Effective WCAG contrast 2–3:1 for thin text at 8px blur. Detection: white shadow + blur > 4px → HIGH; blur > 12px or multiple white shadows → CRITICAL.

SA-CSS-TXS-002: layered near-zero shadows — additive white paint over glyphs

Stacking multiple text-shadow values with 0px blur and near-zero offset but white color creates an additive white layer directly on glyph strokes. Unlike blurred shadows (which spread beyond glyph edges), 0-blur white shadows paint precisely on the glyph, compositing additively with other shadows. Four stacked shadows at rgba(255,255,255,0.25) each create an effective white overlay at approximately 0.68 alpha (Porter-Duff: 1-(1-0.25)^4 ≈ 0.684) on the glyph pixels.

.consent-text {
  color: #111111;
  text-shadow:
    0 0 0 rgba(255, 255, 255, 0.25),
    0 0 1px rgba(255, 255, 255, 0.25),
    0 0 2px rgba(255, 255, 255, 0.25),
    0 0 4px rgba(255, 255, 255, 0.25);
}

/* Additive white shadow composition:
   Each shadow: rgba(255,255,255,0.25) at various small blur radii.
   At glyph pixel, each shadow contributes:
   A_combined = 1 - (1-0.25)^4 = 1 - 0.316 = 0.684

   Effective glyph pixel color (dark text + 0.684 white overlay):
   blend = 0.684 × (1.0, 1.0, 1.0) + (1-0.684) × (17/255, 17/255, 17/255)
         = (0.684 + 0.316×0.067, ...) ≈ (0.705, 0.705, 0.705)
   Luminance of blended glyph: ≈ 0.705×0.705 ≈ 0.456 (sRGB→linear approx)
   Contrast vs white background (L=1.0): 1.05 / (0.456 + 0.05) ≈ 2.08:1 — FAIL

   More aggressive: 6 shadows at rgba(255,255,255,0.3)
   A_combined = 1 - (1-0.3)^6 = 1 - 0.118 = 0.882
   Effective glyph: 0.882×1.0 + 0.118×0.067 ≈ 0.890
   Contrast: 1.05 / (0.890 + 0.05) ≈ 1.12:1 — FAIL

   Looks like a design flourish ("soft glow" effect) — common in UI kits.
   Multiple small shadows at low opacity appear in legitimate stylesheets regularly.

   Detection:
   Parse all text-shadow values; compute aggregate white shadow alpha at glyph pixel.
   Aggregate alpha = 1 - product of (1 - alpha_i) for all white/near-white shadows.
   If aggregate alpha > 0.5 + text color #111111: effective contrast < 2.5:1 → CRITICAL. */
CRITICAL
SA-CSS-TXS-002 — Four stacked rgba(255,255,255,0.25) shadows: aggregate white alpha ≈0.684 on glyph pixels; effective contrast ≈2.08:1 (WCAG fail). Detection: compute aggregate white-shadow alpha across all shadow layers; if aggregate > 0.5 on consent text with dark color → CRITICAL.

SA-CSS-TXS-003: -webkit-text-stroke:2px white — white stroke covers glyph interior

The -webkit-text-stroke property (supported in all major browsers) applies a stroke around text glyph outlines. A white stroke at 2px width straddles the glyph edge: 1px inside and 1px outside the glyph outline. For a consent text character with 1.5px natural stroke width (typical at 14px, font-weight:400), a 2px white stroke covers the entire interior of narrow strokes. The original dark foreground paint is applied after the stroke, partially recovering glyph visibility, but the stroke interference reduces effective contrast at glyph edges — most critically for thin letterforms (i, l, r, t) where the 2px stroke entirely covers the stroke width.

.consent-text {
  color: #111111;
  -webkit-text-stroke: 2px white;  /* white 2px stroke around glyph outlines */
  /* also works without -webkit- prefix in modern browsers: paint-order support */
}

/* Effect on glyph strokes:
   Glyph outline stroke: 2px white, 1px each side of glyph boundary.
   Natural glyph stroke width: ~1.5px at 14px font-weight:400 for most typefaces.

   At a narrow stroke (e.g. the vertical bar of 'i'):
   Natural stroke width: 1.5px
   White -webkit-text-stroke: 2px → covers 1px inside + 1px outside glyph edge.
   Since interior coverage (1px) > half of natural stroke width (0.75px):
   The white stroke covers >50% of the glyph stroke area → interior appears white.
   Foreground fill (#111111) still paints the remaining center pixels.
   Net effect: dark glyph pixels reduced to ≈50% coverage → effective alpha drops.

   For bolder fonts (font-weight:700, stroke ≈ 4px):
   2px white stroke covers 1px of 4px interior → 25% coverage → less severe.
   2px stroke on light fonts causes more damage than on bold.

   Combined attack: -webkit-text-stroke + white glow shadow:
   color: #111111;
   -webkit-text-stroke: 1.5px white;
   text-shadow: 0 0 6px white;
   Combined: stroke removes edge contrast; glow washes residual glyph ink.
   Effective contrast: likely < 2.5:1 for normal-weight text.

   Paint-order note: with paint-order:stroke fill (SVG-style), stroke is painted BELOW fill.
   Without paint-order: stroke is painted over fill (default CSS behavior).
   CSS default (stroke over fill) = more damaging: white stroke covers dark fill.

   Detection:
   Compute -webkit-text-stroke width and color.
   If color is white (or near-white) and width ≥ 1px: HIGH for thin fonts (weight < 400).
   If width ≥ 2px: HIGH regardless of font weight.
   If width ≥ 3px: CRITICAL (covers all but the thickest bold strokes). */

paint-order interaction: SVG text supports paint-order: stroke fill, which paints the stroke below the fill. This partially mitigates the -webkit-text-stroke attack. However, in HTML/CSS text rendering, paint-order is not fully supported outside SVG, so the default stroke-over-fill behavior leaves consent text vulnerable even for attacks that developers might think are "behind" the text.

HIGH
SA-CSS-TXS-003 — -webkit-text-stroke:2px white: covers ≥50% of narrow glyph stroke interior; dark text reduced to partial coverage; effective contrast < 3:1 for thin fonts. Detection: -webkit-text-stroke color white + width ≥ 1px on thin fonts → HIGH; width ≥ 3px → CRITICAL regardless of weight.

SA-CSS-TXS-004: color:white + text-shadow dark — ghost text visibility

Setting the text color to white makes glyphs invisible on a white background. Adding a dark text-shadow provides the only visible representation of the text: a smeared shadow of the glyph shapes at the shadow color opacity. On a white background with a rgba(17,17,17,0.4) shadow at 0–1px blur, glyphs appear as a low-contrast ghostly trace. This mimics "grayed out" text but achieves contrast ratios well below WCAG thresholds. The getComputedStyle(el).color returns white, which appears to be a developer error rather than an intentional attack.

.consent-text {
  color: white;                                      /* invisible on white bg */
  text-shadow: 0 0 1px rgba(17, 17, 17, 0.4);       /* dark shadow provides only visibility */
}

/* Visibility analysis:
   Text foreground: white on white → fully invisible (contrast 1:1).
   Shadow: rgba(17,17,17,0.4) at 0px offset, 1px blur.
   Shadow is the ONLY source of visual glyph information.

   Shadow color rendered behind foreground: at glyph pixel, after foreground paint:
   The white foreground fully covers the shadow where glyph alpha is 1.0.
   At glyph edges (anti-aliased, alpha 0.3–0.7): white foreground partially covers shadow.
   Net visible shadow: visible primarily at anti-aliased glyph edges, not stroke interiors.

   Effective WCAG via shadow edges only:
   Shadow alpha at edge pixel (foreground alpha 0.5): visible_shadow_alpha = 0.4 × (1 - 0.5) = 0.2
   Effective color: rgba(17,17,17,0.2) at edge pixel
   blend_L at edge = 0.2 × 0.003 + 0.8 × 1.0 ≈ 0.801
   Contrast: 1.05 / (0.801 + 0.05) ≈ 1.23:1 — FAIL at edge pixels

   At interior glyph pixel (foreground alpha 1.0): white fully covers shadow → invisible.
   Net: glyph appears as a low-contrast smear of shadow ink at edges only.

   Variant with higher shadow alpha: rgba(17,17,17,0.8) at 0px offset, 0px blur.
   Shadow alpha at edge (foreground alpha 0.5): visible_shadow_alpha = 0.8 × 0.5 = 0.4
   blend_L ≈ 0.4×0.003 + 0.6×1.0 ≈ 0.601; Contrast ≈ 1.05/(0.601+0.05) ≈ 1.62:1 — FAIL
   At interior (foreground alpha 1.0): shadow = 0 (fully covered by white foreground).

   Appears as: visually grayed-out text. May be misread as "disabled" UI element.
   Evades: color check (color:white looks like dev error); opacity check (opacity:1).

   Detectable via: WCAG contrast check — compute foreground+shadow composite contrast.
   If color = white (rgb(255,255,255)) on white background: shadow is the only contrast source.
   Parse text-shadow; check if shadows provide sufficient contrast.
   If shadow contrast (computed as above) < 4.5:1 → CRITICAL SA-CSS-TXS-004. */
CRITICAL
SA-CSS-TXS-004 — color:white + text-shadow:rgba(17,17,17,0.4): consent text invisible as foreground; dark shadow visible only at anti-aliased edges (WCAG ≈1.23:1). Mimics disabled/grayed UI. Detection: color:white on white-background consent element → check shadow for sufficient contrast; if shadow contrast < 4.5:1 → CRITICAL.

Summary of SA-CSS-TXS findings

IDSeverityPatternMechanismWCAG impact
SA-CSS-TXS-001 HIGH text-shadow:0 0 8px white White glow washes glyph stroke ink; severe on thin fonts (weight:300) 2–3:1 for thin text
SA-CSS-TXS-002 CRITICAL 4× rgba(255,255,255,0.25) stacked Aggregate white alpha ≈0.684 on glyph pixels; effective overlay 2.08:1
SA-CSS-TXS-003 HIGH -webkit-text-stroke:2px white White stroke covers ≥50% of narrow glyph stroke interior <3:1 for thin fonts
SA-CSS-TXS-004 CRITICAL color:white + shadow:dark White text invisible; dark shadow provides only edge-pixel visibility 1.23:1 at edges

Related pages