MCP server CSS -webkit-text-fill-color security: transparent text fill override hides consent text, outline-only text via -webkit-text-stroke, selection-invisible text, and SVG fill:none consent text

Published 2026-07-24 — SkillAudit Research

The -webkit-text-fill-color CSS property (now part of CSS Fonts Level 4 as the standardized text-fill-color) overrides the color property specifically for the fill of text glyphs. When -webkit-text-fill-color is set, it takes precedence over color for text rendering — the color property still controls outlines (borders, box-shadows) and other inherited color values, but the actual pixels of the text glyphs use -webkit-text-fill-color. Setting it to transparent makes text glyphs invisible while the color property remains visually irrelevant for glyph rendering.

This creates a deceptive attack surface for MCP servers: getComputedStyle(el).color returns the framework's black (rgb(0, 0, 0)), which most consent detection scripts check. The element has valid dimensions. IntersectionObserver fires. But the actual text is invisible because -webkit-text-fill-color: transparent overrides the glyph fill. Detection requires checking getComputedStyle(el).webkitTextFillColor — a property most consent auditors omit.

Browser support: -webkit-text-fill-color is supported in Chrome 1+, Edge 12+, Safari 3+, and Firefox 49+. It is universally available across all browsers. The standardized text-fill-color (without the -webkit- prefix) is gaining adoption. Both properties are valid attack vectors. See also the related CSS color:transparent attacks.

Attack 1: -webkit-text-fill-color:transparent — invisible consent text with misleading color property

The consent framework sets color: #1a1a1a on the consent panel to ensure dark, readable text. Security scanners check getComputedStyle(el).color and see the expected dark color, confirming the text should be visible. An MCP server sets -webkit-text-fill-color: transparent on the consent element. The glyph fill changes to transparent — text becomes invisible. The color property remains #1a1a1a because -webkit-text-fill-color only overrides glyph fill, not the color inheritance chain. A detection system that only checks getComputedStyle(el).color incorrectly reports the text as visible.

/* Consent framework: */
.consent-panel {
  color: #1a1a1a;       /* dark, readable text */
  background: white;
  padding: 20px;
}

/* MCP attack: */
.consent-panel {
  -webkit-text-fill-color: transparent;
  /* text-fill-color: transparent;  (standardized, also works) */

  /* Effect:
     - color: #1a1a1a still applies (not overridden)
     - -webkit-text-fill-color: transparent overrides glyph fill
     - Text glyphs render with 0 alpha — invisible against white background
     - Element retains dimensions (box model unaffected)
     - IntersectionObserver fires (element in viewport)
     - getComputedStyle(el).color === 'rgb(26, 26, 26)' — misleading
     - getComputedStyle(el).webkitTextFillColor === 'rgba(0, 0, 0, 0)' — reveals attack */
}

// Correct detection:
function detectTextFillColorTransparent(el) {
  const cs = window.getComputedStyle(el);

  // Standard color check — INSUFFICIENT:
  const color = cs.color;  // may be 'rgb(26, 26, 26)' — does NOT reveal the attack

  // Correct check: -webkit-text-fill-color overrides glyph fill
  const textFillColor = cs.getPropertyValue('-webkit-text-fill-color')
    || cs.getPropertyValue('text-fill-color')
    || cs.webkitTextFillColor;

  if (textFillColor) {
    // Parse alpha from rgba()
    const match = textFillColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
    if (match) {
      const alpha = match[4] !== undefined ? parseFloat(match[4]) : 1;
      if (alpha === 0 || textFillColor === 'transparent') {
        console.error('SECURITY: -webkit-text-fill-color is transparent — consent text invisible despite color property being set', {
          element: el, textFillColor, color
        });
        return true;
      }
    }
    if (textFillColor === 'transparent') {
      console.error('SECURITY: -webkit-text-fill-color: transparent on consent element', { el, textFillColor });
      return true;
    }
  }
  return false;
}

Attack 2: -webkit-text-stroke with matching stroke color creates outline-only invisible text

-webkit-text-stroke (CSS Fonts Level 4: text-stroke) adds an outline to text glyphs. Combined with -webkit-text-fill-color: transparent, it produces "outlined text" — text where only the stroke border is visible and the interior fill is transparent. An MCP server exploits this by setting -webkit-text-fill-color: transparent and then also setting -webkit-text-stroke-color to match the background color (white on a white background). Both the fill and the stroke are now background-colored — the text glyphs are completely invisible. But the element's layout, dimensions, and color property all report normal values.

/* MCP attack using text-fill-color:transparent + text-stroke matching background: */
.consent-panel {
  background: #ffffff;
  color: #1a1a1a;                              /* misleading: shows in getComputedStyle */
  -webkit-text-fill-color: transparent;        /* fill: invisible */
  -webkit-text-stroke: 1px #ffffff;            /* stroke: same as background = invisible */
  /* Result: text has no visible fill (transparent) and no visible stroke
     (stroke color matches background). Text glyphs are completely invisible.
     The -webkit-text-stroke adds a tiny 1px outline that is white on white. */
}

/* Alternative: use rgba(255,255,255,0.01) for stroke — barely visible to
   avoid matching exact background color, while still effectively invisible */
.consent-panel {
  -webkit-text-fill-color: rgba(255,255,255,0);
  -webkit-text-stroke: 1px rgba(255,255,255,0.01);
}

// Detection: check both fill-color and stroke-color
function detectTextStrokeInvisible(el) {
  const cs = window.getComputedStyle(el);
  const bgColor = cs.backgroundColor;  // parse to get background rgba

  const strokeColor = cs.getPropertyValue('-webkit-text-stroke-color')
    || cs.getPropertyValue('text-stroke-color');
  const fillColor = cs.getPropertyValue('-webkit-text-fill-color')
    || cs.getPropertyValue('text-fill-color')
    || cs.webkitTextFillColor;

  // Check if stroke color matches background color (invisible stroke)
  if (strokeColor && bgColor && strokeColor !== 'initial') {
    const bg = parseColor(bgColor);
    const stroke = parseColor(strokeColor);
    if (bg && stroke && colorsMatch(bg, stroke)) {
      console.error('SECURITY: -webkit-text-stroke-color matches background — stroke invisible', {
        el, strokeColor, bgColor
      });
      return true;
    }
  }
  return false;
}

function parseColor(str) {
  const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
  return m ? { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 } : null;
}

function colorsMatch(a, b, tolerance = 10) {
  return Math.abs(a.r-b.r) <= tolerance && Math.abs(a.g-b.g) <= tolerance
    && Math.abs(a.b-b.b) <= tolerance && Math.abs(a.a-b.a) <= 0.1;
}

Attack 3: ::selection { color:transparent } — consent text invisible when selected

The ::selection pseudo-element controls the appearance of selected text. A less obvious variant of the text-fill-color attack uses ::selection { -webkit-text-fill-color: transparent; color: transparent } on the consent element. When the user selects the consent text (e.g., to copy it or read it more carefully), the selected text renders invisible in the selection highlight. Combined with a selection background that also matches the regular background, the user perceives an empty selection area. This attack doesn't hide the text in its default state but prevents users from reading it via selection-based scrutiny — relevant when the text is technically visible at a small font size but users rely on selection to zoom in.

/* Consent framework: */
.consent-panel {
  color: #1a1a1a;
  font-size: 9px;    /* very small — technically "visible" */
}

/* MCP attack: make selected text invisible */
.consent-panel::selection {
  -webkit-text-fill-color: transparent;
  color: transparent;
  background-color: #f0f0f0;   /* light gray — looks like empty selection */
}

/* Effect: when user triple-clicks to select all consent text and tries to read it
   by looking at the selection, the selected text is invisible (transparent) against
   the light-gray selection background. The user cannot read the text by selecting it.
   This bypasses the strategy of "select the text to see it better." */

/* Note: ::selection is partially restricted — browsers limit which properties
   can be set. color, background-color, text-decoration-color, and
   -webkit-text-fill-color are generally allowed. */

// Detection: check ::selection styling on consent elements
// (Cannot be checked via getComputedStyle — must inspect CSSRules)
function detectSelectionHiding(consentSelector = '.consent-panel') {
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        const sel = rule.selectorText || '';
        if (sel.includes('::selection') && sel.includes(consentSelector.replace('.', ''))) {
          const color = rule.style.getPropertyValue('color');
          const fillColor = rule.style.getPropertyValue('-webkit-text-fill-color');
          if (color === 'transparent' || fillColor === 'transparent') {
            console.error('SECURITY: ::selection makes consent text transparent when selected', {
              selector: sel, color, fillColor
            });
            return true;
          }
        }
      }
    } catch (e) {}
  }
  return false;
}

Attack 4: SVG consent text with fill:none — SVG-rendered disclosure invisible

Some consent frameworks render consent disclosure text as SVG (for custom typography, precise layout control, or print-quality rendering). SVG text uses fill (not color) to control glyph rendering. The CSS color property can be aliased to SVG fill via fill: currentColor, but if the MCP server overrides fill: none on SVG text elements, the glyphs lose their fill. getComputedStyle().color returns the framework's color; SVG text is invisible because fill: none applies at the SVG rendering level. This is distinct from -webkit-text-fill-color but produces the same effect via the SVG paint model.

/* Consent rendered as SVG text: */
/* <svg class="consent-svg">
     <text class="consent-text">By using this service you agree to...</text>
   </svg> */

/* Consent framework: */
.consent-text {
  fill: currentColor;   /* inherits color property */
}
.consent-svg {
  color: #1a1a1a;
}

/* MCP attack: override SVG text fill with none */
.consent-text {
  fill: none;
  /* SVG text with fill:none renders invisible glyphs.
     stroke: none (default) — no stroke either.
     The SVG text element exists in the DOM, has valid bbox dimensions
     (el.getBBox() returns { width: 300, height: 18, ... }),
     but the text is invisible.
     getComputedStyle(svgTextEl).fill may return 'none'.
     getComputedStyle(svgTextEl).color returns '#1a1a1a' — misleading. */
}

// Detection for SVG consent text
function detectSvgTextFillNone(svgEl) {
  const textEls = svgEl.querySelectorAll('text, tspan, textPath');
  for (const el of textEls) {
    const cs = window.getComputedStyle(el);
    const fill = cs.getPropertyValue('fill');
    // fill:none means no glyph fill — invisible text
    if (fill === 'none') {
      console.error('SECURITY: SVG consent text has fill:none — glyphs invisible', { el, fill });
      return true;
    }
    // Also check for transparent fill
    const fillOpacity = parseFloat(cs.getPropertyValue('fill-opacity') || '1');
    if (fillOpacity === 0) {
      console.error('SECURITY: SVG consent text has fill-opacity:0 — glyphs invisible', { el, fillOpacity });
      return true;
    }
  }
  return false;
}

Why text-fill-color attacks evade standard detection: The canonical detection pattern for "is consent text visible?" checks: element dimensions (non-zero), position in viewport (IntersectionObserver), and getComputedStyle().color for a non-transparent value. The -webkit-text-fill-color attack passes all three: the element has dimensions, it's in the viewport, and color is non-transparent. Only explicitly checking getComputedStyle(el).webkitTextFillColor reveals the attack. Most open-source consent auditing libraries do not include this check. Consent framework vendors should add it immediately.

Attack summary

Attack Mechanism Effect Severity
-webkit-text-fill-color:transparent Glyph fill overridden to transparent; color property unchanged Consent text invisible; getComputedStyle().color misleadingly reports visible color High
Text-stroke color matching background -webkit-text-fill-color:transparent + stroke matching background Fill invisible, stroke also invisible — glyphs completely unrendered High
::selection transparent text ::selection { color: transparent } on consent elements Selected consent text invisible — users cannot read by selecting Medium
SVG fill:none on text elements fill: none on <text> elements in SVG consent SVG glyph fill removed; text invisible; color property unchanged High

Consolidated finding blocks

High CSS -webkit-text-fill-color:transparent consent text invisibility attack: MCP server applies -webkit-text-fill-color: transparent to the consent panel. The property overrides color for text glyph rendering — consent text becomes invisible against the background. The color property remains #1a1a1a (the framework's dark text color), causing standard getComputedStyle().color checks to falsely report the text as visible. Detection requires explicitly checking getComputedStyle(el).webkitTextFillColor or getComputedStyle(el).textFillColor for a transparent value.
High CSS -webkit-text-fill-color:transparent + -webkit-text-stroke matching background: MCP applies both -webkit-text-fill-color: transparent and -webkit-text-stroke: 1px rgba(255,255,255,0.01). The fill is invisible (transparent) and the stroke is indistinguishable from the white background. Text glyphs render with no visible pixels. Detection requires checking both webkitTextFillColor and comparing webkitTextStrokeColor against the computed background-color for a near-match.
Medium CSS ::selection transparent consent text selection hiding: MCP applies ::selection { color: transparent; background-color: #f0f0f0 } to the consent panel. When users select the consent text (to examine it more closely or copy it), the selected text renders invisible against the light-gray selection background. The consent text may be technically visible in its normal state (small font) but unreadable when selected. Detection requires iterating CSSRules for ::selection rules targeting consent elements — getComputedStyle() does not expose ::selection styles.
High SVG consent text fill:none paint-model hiding: MCP applies fill: none to SVG <text> elements used by the consent framework for custom typography. SVG text rendering uses the fill property (not color) to paint glyphs. With fill: none, glyphs have no paint server — they are invisible. The element has valid SVG bounding box dimensions (getBBox() returns non-zero width/height). Standard HTML consent auditors checking getComputedStyle().color do not detect this SVG-specific hiding mechanism.

← Blog  |  CSS color:transparent attacks  |  CSS opacity attacks  |  Security Checklist