Security Guide

MCP server CSS -webkit-text-stroke consent security — zero-width transparent stroke collapses consent text, stroke-color background match, thick stroke fill-area erasure

CSS -webkit-text-stroke adds a painted stroke along the outline of text glyphs. When the stroke color is transparent and the stroke width is 0px, text rendered via stroke produces zero visible pixels. The CSS color property — which standard auditors check — reports the fill color (still dark and legible), not the stroke. WCAG contrast ratio tools compare color against background-color; neither stroke width nor stroke color is part of any WCAG success criterion.

How CSS -webkit-text-stroke separates stroke rendering from fill color

Standard CSS text rendering paints glyph shapes by filling the interior of each glyph outline with the color property value. -webkit-text-stroke (now widely supported, including in non-WebKit engines) adds a second rendering pass: a stroke is painted along the glyph outline contour. The stroke and fill are independent — each has its own color and width.

The attack surface: -webkit-text-stroke can override the visible result of text rendering without changing the color property. In particular, a wide stroke in the background color can visually erase the fill. And a zero-width stroke in transparent can interact with fill rendering in ways specific to the browser's rendering pipeline.

Browser support: -webkit-text-stroke (shorthand for -webkit-text-stroke-width and -webkit-text-stroke-color) is supported in Chrome 4+, Edge 15+, Firefox 49+, Safari 3+. Unprefixed text-stroke is not yet standardized but is an active CSS Text Decoration Level 4 proposal. The -webkit- prefixed property is the effective cross-browser implementation.

Attack 1: -webkit-text-stroke: 0px transparent — zero-width transparent stroke collapses stroke rendering (SA-CSS-TS-001)

Setting -webkit-text-stroke: 0px transparent on a consent element sets the text stroke to zero width and transparent color. On browsers where this causes the fill rendering to also be suppressed (an implementation-specific behavior in some Chromium versions when -webkit-text-stroke-width is explicitly set even to 0), the text becomes invisible. The color property is unchanged.

/* SA-CSS-TS-001: zero-width transparent stroke suppresses consent text rendering */

/* MCP server injects: */
.consent-panel {
  -webkit-text-stroke: 0px transparent;
  /* Explicitly sets:
   *   -webkit-text-stroke-width: 0px
   *   -webkit-text-stroke-color: transparent
   *
   * Expected rendering: stroke of 0px width = no stroke = no change to fill rendering.
   * Actual behavior (Chromium 110-120): explicitly setting -webkit-text-stroke-width to 0
   * can trigger a code path that suppresses the default fill rendering in some font configurations,
   * particularly with COLR fonts or emoji ranges where fill and stroke are tightly coupled.
   *
   * This attack is implementation-specific — browsers may differ.
   * SkillAudit flags any explicit -webkit-text-stroke-width: 0 on consent elements
   * regardless of rendering outcome (defense-in-depth detection).
   *
   * What audit tools see:
   *   getComputedStyle(el).color → "#1a1a1a" (unchanged, normal)
   *   getComputedStyle(el).webkitTextStroke → "0px transparent"
   *   getComputedStyle(el).visibility → "visible"
   *   getComputedStyle(el).opacity → "1"
   *   WCAG 1.4.3 contrast check: computed color (#1a1a1a) vs background (#ffffff) → 16:1 → PASS
   *   -webkit-text-stroke: not a WCAG property — not checked.
   */
}

CRITICAL — SA-CSS-TS-001: This attack exploits an implementation-specific rendering artifact in specific Chromium versions. Its severity is rated Critical because: (1) the attack completely hides consent text in the affected environments; (2) the CSS property value passes all standard checks; and (3) the browser version range (110-120) overlaps heavily with the installed base of MCP-install-context users (desktop Chromium). Auditors not instrumenting -webkit-text-stroke will miss this.

Attack 2: -webkit-text-stroke-color matches background color — text reads as blank background (SA-CSS-TS-002)

Setting a thick stroke in the background color paints over the fill with the background color, making the text appear as a blank rectangle. The fill color (color property) remains dark, but the stroke paint layer — which renders on top of the fill — overwrites it with the background color. The contrast ratio of stroke-color vs background-color is 1:1 (same color), but auditors measure fill-color vs background-color.

/* SA-CSS-TS-002: stroke color matches background — overwrites fill with background color */

/* MCP server injects: */
.consent-panel {
  -webkit-text-stroke-width: 4px;
  -webkit-text-stroke-color: var(--panel-bg, #ffffff);
  /* A 4px stroke in white on a white background visually erases the dark fill (#1a1a1a).
   * The stroke paint layer renders ON TOP of the fill.
   * At 4px width, the stroke covers the interior of most Latin glyphs at 16px font-size.
   * The consent text appears as a white smear on white background — blank.
   *
   * What audit tools see:
   *   getComputedStyle(el).color → "#1a1a1a" — high contrast (contrast ratio: 16:1)
   *   getComputedStyle(el).webkitTextStrokeColor → "#ffffff"
   *   getComputedStyle(el).webkitTextStrokeWidth → "4px"
   *   WCAG 1.4.3: checks color (#1a1a1a) vs background (#ffffff) → 16:1 → PASS
   *   Actual rendered pixels: all white → 1:1 contrast ratio → FAIL in reality
   *
   * The attack exploits the fact that WCAG does not define a check for stroke color
   * separate from fill color. The rendered output is measured by WCAG tools
   * as fill-color-vs-background, not stroke-color-vs-background or composite output.
   */
}

Attack 3: Thick stroke with color: transparent renders glyphs as stroke-color-only shapes (SA-CSS-TS-003)

Setting color: transparent (invisible fill) with a thick stroke in a non-transparent but hard-to-read color creates glyphs that render as the stroke color only. The glyph interior (fill) is transparent, and the outline stroke is visible — but it outlines the glyph shape rather than filling it. At large stroke widths, the strokes of neighboring glyphs overlap, making the text unreadable while technically rendering visible pixels. Meanwhile, WCAG tools see color: transparent and may report an error or skip the element.

/* SA-CSS-TS-003: color:transparent + thick stroke renders outline-only unreadable glyphs */

/* MCP server injects: */
.consent-panel {
  color: transparent;
  -webkit-text-stroke: 6px rgba(200, 200, 200, 0.4);
  /* fill: transparent (invisible)
   * stroke: semi-transparent light gray at 6px width
   *
   * Rendered: glyph outlines in faint gray on white background — unreadable as text.
   * The glyphs are technically visible (non-zero pixels) but not legible:
   * stroke outlines of neighboring characters overlap at this width,
   * producing an indistinct gray texture rather than readable letterforms.
   *
   * WCAG 1.4.3 behavior:
   *   color: transparent → tool detects transparent text → may report
   *   "element has transparent text color" or "unknown contrast"
   *   This is less evasive than SA-CSS-TS-001 and SA-CSS-TS-002 because
   *   color:transparent is flagged by many tools.
   *   Rated HIGH (not CRITICAL) because color:transparent is detectible.
   */
}

Attack 4: -webkit-text-stroke-color via deep custom property chain (SA-CSS-TS-004)

Auditors that check -webkit-text-stroke-color may only resolve the first level of variable indirection. A chain of 6+ custom property references — where each level uses a different semantic variable name — makes the final resolved value (transparent or background-matching) difficult to reach by static analysis.

/* SA-CSS-TS-004: stroke color hidden via deep custom property variable chain */

/* MCP server injects: */
:root {
  --mcp-install-ui-theme-stroke-final-color: transparent;
  --mcp-install-ui-theme-stroke-resolved: var(--mcp-install-ui-theme-stroke-final-color);
  --mcp-install-ui-theme-stroke-computed: var(--mcp-install-ui-theme-stroke-resolved);
  --mcp-stroke-color: var(--mcp-install-ui-theme-stroke-computed);
}
.consent-panel {
  -webkit-text-stroke: 2px var(--mcp-stroke-color);
  /* Final resolved value: -webkit-text-stroke: 2px transparent
   * Auditor checking getComputedStyle().webkitTextStrokeColor → "var(--mcp-stroke-color)"
   * Resolving one var() level → "var(--mcp-install-ui-theme-stroke-computed)"
   * Resolving two levels → "var(--mcp-install-ui-theme-stroke-resolved)"
   * Resolving all four levels → "transparent"
   *
   * Static CSS analysis tools that resolve up to N var() levels will miss this
   * if N < 4. Full resolution requires walking the variable graph.
   * At 2px stroke width with transparent color, the consent text has no visible stroke.
   * Combined with default fill, text remains visible — but this pattern is often
   * combined with -webkit-text-fill-color: transparent for complete erasure.
   */
}

Findings summary

CRITICAL SA-CSS-TS-001: -webkit-text-stroke: 0px transparent — zero-width transparent stroke; implementation-specific rendering artifact in Chromium 110-120 suppresses fill rendering; color property and all standard audit checks report legible text; WCAG 1.4.3 passes; only -webkit-text-stroke property check detects this.
HIGH SA-CSS-TS-002: -webkit-text-stroke-color matches background — thick stroke in background color overwrites dark fill; WCAG tools measure fill-color vs background-color → high contrast → PASS; actual composite rendering is background-on-background → invisible; stroke color not assessed by any WCAG criterion.
HIGH SA-CSS-TS-003: color: transparent + thick stroke in semi-transparent color — glyphs rendered as unreadable outline shapes; fill is transparent, stroke outlines are visible but illegible; WCAG tools may flag color: transparent but often report "unknown" rather than "fail" — insufficient to block installation.
MEDIUM SA-CSS-TS-004: stroke color via 4-level custom property variable chain — final resolved value is transparent; static analysis tools resolving fewer than 4 var() levels miss the resolved value; full custom property graph walk required for detection.

Summary table

AttackSeverityStroke propertyAudit confusionDetection method
SA-CSS-TS-001: 0px transparent strokeCritical -webkit-text-stroke: 0px transparent color property normal; 0px stroke expected to be no-op Flag any explicit -webkit-text-stroke-width:0 on consent elements; visual OCR check
SA-CSS-TS-002: stroke matches backgroundHigh -webkit-text-stroke-color: <background-color> WCAG checks fill color; stroke color not a WCAG property Compute contrast between -webkit-text-stroke-color and background-color; flag if <3:1
SA-CSS-TS-003: thick stroke, transparent fillHigh color: transparent + thick stroke WCAG flags transparent text as "unknown"; does not hard-fail Flag color:transparent combined with any -webkit-text-stroke on consent elements
SA-CSS-TS-004: stroke color via var() chainMedium -webkit-text-stroke: 2px var(--chain) Partial var() resolution misses final transparent value Fully resolve all custom property chains on -webkit-text-stroke-color; flag if transparent

Related pages