Security reference · CSS injection · Text rendering · Consent hiding

MCP server CSS -webkit-text-stroke security

CSS -webkit-text-fill-color and -webkit-text-stroke are vendor-prefixed properties that control glyph fill and outline independently of the standard color property. When -webkit-text-fill-color is set to transparent, the glyph fills become invisible while the element passes a check of getComputedStyle().color — because color and webkitTextFillColor are different computed properties. An auditor checking getComputedStyle(el).color for transparency will see the standard color value (e.g., rgb(0,0,0)) and pass the element, even though the actual rendered text fill is transparent. Four attack patterns: transparent fill hollow text, white stroke and fill on white background, near-zero alpha fill, and JS fill color removal at mousedown.

CSS text-fill-color and text-stroke fundamentals

The standard CSS color property sets both the fill and notional outline color of text glyphs. The webkit-prefixed extensions separate these two components: -webkit-text-fill-color controls the interior fill of each glyph, and -webkit-text-stroke (shorthand for -webkit-text-stroke-width + -webkit-text-stroke-color) controls the outline drawn around the glyph path. When -webkit-text-fill-color is set, it overrides the fill portion of color. A glyph with color: black; -webkit-text-fill-color: transparent renders as a hollow black outline with no interior fill — like letterpress impression on paper.

Properties setVisual resultgetComputedStyle().colorgetComputedStyle().webkitTextFillColor
color: blackNormal black textrgb(0,0,0)rgb(0,0,0) (inherits)
-webkit-text-fill-color: transparentHollow outline onlyrgb(0,0,0) (unchanged)rgba(0,0,0,0) transparent
-webkit-text-fill-color: white; background: whiteInvisible white on whitergb(0,0,0) (unchanged)rgb(255,255,255)
-webkit-text-fill-color: rgba(0,0,0,0.01)99% invisible fillrgb(0,0,0) (unchanged)rgba(0,0,0,0.01)

Attack surface: SA-CSS-TXSK-001 — transparent fill hollow outline text

-webkit-text-fill-color: transparent (with or without a non-zero stroke width) makes all glyph fills invisible while preserving the standard color property value. A consent element with color: #1a1a1a; -webkit-text-fill-color: transparent passes an auditor check of getComputedStyle(el).color — which returns rgb(26,26,26), a perfectly normal dark text color. Only a check of getComputedStyle(el).webkitTextFillColor reveals the transparent fill. Without a visible stroke (-webkit-text-stroke-width: 0), the element renders as completely blank — no fill, no outline. With a very thin stroke (0.1px), a barely-visible hairline outline may appear on high-DPI screens.

/* SA-CSS-TXSK-001: hollow text via transparent fill */
.consent-disclosure {
  color: #1a1a1a;                      /* passes color check */
  -webkit-text-fill-color: transparent; /* fill invisible */
  -webkit-text-stroke-width: 0;         /* no outline */
  /* Rendered result: completely blank space */
}

Detection: getComputedStyle(consentEl).webkitTextFillColor — parse the rgba() value and check alpha < 0.1. Also check for webkitTextFillColor === 'transparent' or alpha zero.

Attack surface: SA-CSS-TXSK-002 — white stroke and fill on white background

All visible ink is made white: -webkit-text-stroke: 3px white; -webkit-text-fill-color: white; background: white. A heavy white stroke at 3px combined with a white fill creates fully white text on a white background — invisible. The standard color property may remain dark (e.g., color: #333) so that an auditor checking only getComputedStyle().color sees a normal dark text color. The actual rendered fill comes from webkitTextFillColor which overrides color's fill contribution.

/* SA-CSS-TXSK-002: white-on-white using separate fill and stroke */
.mcp-consent {
  color: #333333;                   /* auditor sees normal dark color */
  -webkit-text-fill-color: white;   /* actual fill: white */
  -webkit-text-stroke: 3px white;   /* stroke: white — no visible outline */
  background: white;                /* element background: white */
  /* Rendered: white text on white background = invisible */
}

A scanner checking for color: white or color: #fff in the stylesheet does not find it — the attack is in -webkit-text-fill-color. Detection must check both properties independently and compare their effective alpha and contrast against the background.

Attack surface: SA-CSS-TXSK-003 — near-zero alpha fill

-webkit-text-fill-color: rgba(0,0,0,0.01) sets a 1% opacity fill — not literally transparent, not literally rgba(0,0,0,0), but visually indistinguishable from invisible on any practical screen. This passes checks of the form webkitTextFillColor === 'transparent' or alpha-channel extraction with an insufficiently tight threshold. At 1% opacity on a white background, the effective contrast ratio is approximately 1.01:1 — far below any legibility standard. Custom property obfuscation: --mcp-consent-ink: rgba(0,0,0,0.01); -webkit-text-fill-color: var(--mcp-consent-ink).

/* SA-CSS-TXSK-003: near-zero alpha — not "transparent" keyword */
:root { --mcp-ink-alpha: rgba(0, 0, 0, 0.01); }
.consent-text {
  -webkit-text-fill-color: var(--mcp-ink-alpha);
  /* getComputedStyle().webkitTextFillColor: "rgba(0, 0, 0, 0.01)" */
  /* Not === 'transparent' — literal match check fails */
  /* Contrast ratio ~1.01:1 — threshold check required */
}

Detection: Parse getComputedStyle(el).webkitTextFillColor as rgba() and extract the alpha channel. Flag any alpha value < 0.1 on consent-bearing text elements. Do not rely on literal "transparent" string match.

Attack surface: SA-CSS-TXSK-004 — JS mousedown fill color removal

The consent element starts with a normal text fill at page load. MCP JS attaches a mousedown listener to the install button that, at interaction time, sets -webkit-text-fill-color: transparent on the consent element — making the text instantly invisible when the user initiates install. The style attribute changes from containing no -webkit-text-fill-color declaration to containing -webkit-text-fill-color: transparent. A MutationObserver on the style attribute detects this change. Without a MutationObserver, any load-time audit sees normal consent text with no fill-color issues.

/* SA-CSS-TXSK-004: JS fill removal at mousedown */
// MCP JS — fires at install button press:
installBtn.addEventListener('mousedown', () => {
  consentEl.style.webkitTextFillColor = 'transparent';
  // consent text fill becomes invisible before click fires
  // DOM text still present; install completes
});

// Detection:
const obs = new MutationObserver(() => {
  const fill = getComputedStyle(consentEl).webkitTextFillColor;
  const alpha = /* parse rgba alpha from fill string */;
  if (alpha < 0.1) abort(); // SA-CSS-TXSK-004
});
obs.observe(consentEl, { attributes: true, attributeFilter: ['style'] });

Findings summary

Critical
SA-CSS-TXSK-001 — -webkit-text-fill-color:transparent makes glyph fills invisible; getComputedStyle().color returns normal dark value — standard color check misses it. Check webkitTextFillColor separately.
Critical
SA-CSS-TXSK-002 — white fill + white stroke on white background; color property set to dark value to evade simple color check; rendered text is fully invisible.
High
SA-CSS-TXSK-003 — near-zero alpha fill (rgba(0,0,0,0.01)); not literally 'transparent' keyword; passes literal-match check; contrast ratio ~1.01:1; threshold check at alpha < 0.1 required.
Critical
SA-CSS-TXSK-004 — JS mousedown sets text-fill-color to transparent at install time; consent invisible at interaction; passes all load-time audits. MutationObserver on style attribute required.

Key insight

Always check both getComputedStyle(el).color and getComputedStyle(el).webkitTextFillColor. When -webkit-text-fill-color is set, it overrides the fill contribution of color — the standard color check is insufficient. Parse both values as rgba() and check: (1) alpha < 0.1, (2) compound contrast ratio of fill color against effective background < 1.5:1.

See also: color:transparent attacks, color opacity and rgba attacks, -webkit-text-fill-color security reference.