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 set | Visual result | getComputedStyle().color | getComputedStyle().webkitTextFillColor |
|---|---|---|---|
| color: black | Normal black text | rgb(0,0,0) | rgb(0,0,0) (inherits) |
| -webkit-text-fill-color: transparent | Hollow outline only | rgb(0,0,0) (unchanged) | rgba(0,0,0,0) transparent |
| -webkit-text-fill-color: white; background: white | Invisible white on white | rgb(0,0,0) (unchanged) | rgb(255,255,255) |
| -webkit-text-fill-color: rgba(0,0,0,0.01) | 99% invisible fill | rgb(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
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.