Security reference · CSS injection · Background clip · Text painting · Consent hiding

MCP server CSS background-clip consent security

CSS background-clip: text combined with color: transparent transforms how text glyphs are filled: instead of the CSS color property providing the fill, the background-image is clipped to the exact shape of each glyph and used as the fill source. This technique is legitimately used for gradient text effects. In a malicious MCP server installer it becomes an invisibility weapon: set background-image: linear-gradient(white, white) and color: transparent on a white background — every consent glyph is filled with white and becomes completely invisible. The critical failure is that getComputedStyle(el).color returns 'transparent', not the background-image color — WCAG contrast algorithms receive transparent as the foreground color and fall back to treating it as the background color, reporting a 1:1 contrast ratio as "passing" in some implementations rather than "indeterminate."

background-clip: text attack surface overview

Attack variantCSS valuesEffect on consent textgetComputedStyle().color
White gradient on white backgroundbackground-image: linear-gradient(#fff,#fff); color: transparent; -webkit-background-clip: textAll glyphs filled with white — completely invisibletransparent
Custom property indirectioncolor: var(--consent-text-color) where --consent-text-color: transparentSame as above; property chain hides transparent valuetransparent
Background-image data URL PNGbackground-image: url('data:image/png;base64,...') (transparent PNG)Glyphs filled with transparent PNG alpha — invisibletransparent
CSS Paint Workletbackground-image: paint(consentHider)Worklet returns white paint at install time; static analysis cannot evaluate worklet outputtransparent

background-clip: text breaks all color-based consent checks: Standard WCAG contrast audits compare getComputedStyle(el).color against the background. With color: transparent and background-clip: text, the contrast check receives transparent as the foreground — the actual rendered pixel color is in the background-image, which no standard audit tool reads. The consent text is invisible in the browser but passes automated color-contrast checks.

Attack 1: white gradient + color: transparent — consent text rendered invisible

The core attack requires only three CSS properties applied to the consent element. The -webkit- prefixed variant (-webkit-background-clip: text) is required for WebKit/Blink in addition to the unprefixed version, which has been the standard since Chrome 120. Both must be checked:

/* Malicious CSS — SA-CSS-BC-001 */
.mcp-consent-text {
  /* These three properties together make all consent text invisible */

  /* 1. Replace glyph fill source with background-image */
  -webkit-background-clip: text;   /* Chrome, Safari, Edge — required */
  background-clip: text;           /* Standard — Chrome 120+, Firefox 122+ */

  /* 2. Remove CSS color so background-image shows through */
  color: transparent;
  -webkit-text-fill-color: transparent; /* extra override in some WebKit builds */

  /* 3. Use a background-image that matches the page background */
  background-image: linear-gradient(to right, #ffffff, #ffffff);
  /* On a white (#fff) page background: every glyph is filled with white.
     The text is completely invisible — same color as the background.
     But it's present in the DOM and passes font-size, display, visibility checks. */
}

/* Comparison: what DOM checks return vs. what would reveal the attack:
   ❌ getComputedStyle(el).color                     → "rgba(0, 0, 0, 0)" or "transparent"
   ❌ getComputedStyle(el).visibility                → "visible"
   ❌ el.getBoundingClientRect()                     → normal rect
   ❌ el.textContent                                 → full consent text
   ❌ el.checkVisibility()                           → true in some implementations
   ✅ getComputedStyle(el).backgroundClip            → "text" — THIS is the signal
   ✅ getComputedStyle(el).webkitBackgroundClip      → "text"
   ✅ getComputedStyle(el).backgroundImage           → "linear-gradient(..."
   ✅ color === 'transparent' AND backgroundClip includes 'text' → ATTACK
*/

function detectBackgroundClipTextAttack(root = document) {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;

  for (const el of root.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const cs = getComputedStyle(el);

    const bgClip = (cs.backgroundClip || '') + (cs.webkitBackgroundClip || '');
    const color  = cs.color;
    const textFill = cs.webkitTextFillColor || '';

    const clipIsText   = bgClip.includes('text');
    const colorIsTransparent = (color === 'transparent' || color === 'rgba(0, 0, 0, 0)');
    const fillIsTransparent  = (!textFill || textFill === 'transparent' || textFill === 'rgba(0, 0, 0, 0)');

    if (clipIsText && colorIsTransparent && fillIsTransparent) {
      findings.push({ id: 'SA-CSS-BC-001', severity: 'high',
        message: `Consent element has background-clip: text with color: transparent. Glyph fill is sourced from background-image — if background-image matches the page background, all consent text is invisible. WCAG color contrast check receives 'transparent' as foreground and cannot evaluate real contrast.` });
    }
  }
  return findings;
}

Attack 2: CSS custom property indirection — hiding transparent in a var() chain

A more sophisticated variant stores the transparent value inside a CSS custom property and applies it via var(). Static CSS analyzers that scan stylesheet text for color: transparent will find nothing suspicious — the custom property assignment is in a different rule block, potentially loaded from a separate stylesheet or injected at runtime. Only getComputedStyle() evaluation (which resolves var() chains) reveals the final computed value:

/* Malicious CSS — SA-CSS-BC-002 */

/* Custom property set at root or a distant ancestor */
:root {
  --consent-text-color: transparent;
  --consent-bg-fill: linear-gradient(#fff, #fff);
}

/* Applied to consent element — static analysis sees only var() references */
.mcp-consent-disclosure {
  -webkit-background-clip: text;
  background-clip: text;
  color: var(--consent-text-color);              /* resolves to transparent */
  background-image: var(--consent-bg-fill);      /* resolves to white gradient */
}

/* Even more sophisticated: the custom property is set via JS at runtime */
document.documentElement.style.setProperty('--consent-text-color', 'transparent');
document.documentElement.style.setProperty('--consent-bg-fill', 'linear-gradient(white, white)');

/* Detection via getComputedStyle still works — var() chains are resolved:
   getComputedStyle(consentEl).color → "rgba(0, 0, 0, 0)" regardless of var() depth
   getComputedStyle(consentEl).backgroundClip → "text"

   The SA-CSS-BC-001 detection function above handles this case correctly
   because it calls getComputedStyle() rather than reading raw stylesheet text.
   No special case needed for custom property indirection. */

Attack 3: transparent PNG data URL background — image-sourced glyph fill

Instead of a gradient, a small transparent PNG encoded as a data URL provides the background-image. The PNG is 1×1 pixels with alpha 0 — completely transparent. When this is clipped to glyph shapes, each character is filled with a transparent image. This variant evades gradient-specific detection that checks background-image for linear-gradient(white, white)-style patterns, because the value is an opaque base64-encoded data URL. The rendered output is identical: invisible consent text:

/* Malicious CSS — SA-CSS-BC-003 */
.mcp-consent-text {
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;

  /* 1x1 fully transparent PNG as base64 data URL */
  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
  background-size: cover;
  /* The PNG has alpha=0. Every glyph is filled with this transparent image.
     The consent text renders with zero opacity — completely invisible.
     getComputedStyle(el).backgroundImage returns the full data URL — a text
     classifier can flag 'data:image/png' + 'background-clip: text' + 'color: transparent'. */
}

/* Detection — extend base detector to handle data URL variant */
function detectDataUrlBackgroundClip(el) {
  const cs = getComputedStyle(el);
  const bgClip  = (cs.backgroundClip || '') + (cs.webkitBackgroundClip || '');
  const bgImage = cs.backgroundImage || '';
  const color   = cs.color;

  if (!bgClip.includes('text')) return null;
  if (color !== 'transparent' && color !== 'rgba(0, 0, 0, 0)') return null;

  if (bgImage.startsWith('url(') && bgImage.includes('data:image/')) {
    return { id: 'SA-CSS-BC-003', severity: 'high',
      message: `Consent element has background-clip: text with color: transparent and background-image: data URL. A data URL image may be transparent — glyph fill is sourced from an embedded image file that may render consent text invisible.` };
  }
  return null;
}

Attack 4: CSS Houdini Paint Worklet — worklet generates invisible paint at install time

CSS Houdini Paint Worklets allow arbitrary JavaScript to generate background-image values at paint time. A malicious skill registers a worklet named consentHider that returns a white or transparent fill. The background-image: paint(consentHider) value in CSS is a function call to this worklet. Static analysis of the stylesheet finds only the opaque string 'paint(consentHider)' — the actual rendered color depends on runtime worklet execution that cannot be determined from the CSS value. Combined with background-clip: text and color: transparent, the worklet makes consent text invisible with no statically analyzable color value:

/* Malicious worklet — registered in JS */
CSS.paintWorklet.addModule('data:application/javascript,' + encodeURIComponent(`
  class ConsentHiderPainter {
    paint(ctx, geom, properties) {
      ctx.fillStyle = '#ffffff'; // white fill — or 'rgba(0,0,0,0)' for transparent
      ctx.fillRect(0, 0, geom.width, geom.height);
    }
  }
  registerPaint('consentHider', ConsentHiderPainter);
`));

/* CSS — SA-CSS-BC-004 */
.mcp-consent-disclosure {
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  background-image: paint(consentHider); /* value is opaque to static analysis */
}

/* Detection for Paint Worklet variant:
   getComputedStyle(el).backgroundImage will return "paint(consentHider)"
   in supporting browsers. Detect the pattern: background-clip:text +
   color:transparent + background-image that starts with 'paint(' */
function detectPaintWorkletBackgroundClip(el) {
  const cs = getComputedStyle(el);
  const bgClip  = (cs.backgroundClip || '') + (cs.webkitBackgroundClip || '');
  const bgImage = cs.backgroundImage || '';
  const color   = cs.color;

  if (!bgClip.includes('text')) return null;
  if (color !== 'transparent' && color !== 'rgba(0, 0, 0, 0)') return null;

  if (bgImage.startsWith('paint(')) {
    return { id: 'SA-CSS-BC-004', severity: 'high',
      message: `Consent element has background-clip: text with color: transparent and background-image: paint() worklet. Worklet return value cannot be statically analyzed — the rendered glyph fill may be white or transparent, making consent text invisible.` };
  }
  return null;
}

background-clip: text defeats every color-based consent audit: WCAG 1.4.3 contrast algorithms, accessibility tree color checks, and color-blindness simulators all source their foreground color from getComputedStyle(el).color. With color: transparent, all of these receive a transparent foreground and cannot compute a meaningful contrast ratio. The only reliable detection is to check backgroundClip directly and flag any consent element where background-clip: text is combined with color: transparent.

SkillAudit findings for CSS background-clip consent attacks

HighSA-CSS-BC-001 — Consent element has background-clip: text with color: transparent. Glyph fill is sourced from background-image; if that image matches or nearly matches the page background, all consent text is visually invisible. WCAG color contrast checks receive transparent as the foreground value and cannot evaluate real contrast.
HighSA-CSS-BC-002 — Consent element color resolves to transparent via a CSS custom property chain (var()) combined with background-clip: text. Static stylesheet analysis does not resolve var() chains — only getComputedStyle() evaluation reveals the transparent computed value. Attack is otherwise identical to SA-CSS-BC-001.
HighSA-CSS-BC-003 — Consent element has background-clip: text with color: transparent and background-image set to a data: URL image. The embedded image may be transparent or white — its rendered color cannot be determined from the URL string without decoding and rendering the image data.
HighSA-CSS-BC-004 — Consent element has background-clip: text with color: transparent and background-image: paint() Houdini worklet reference. Worklet return color cannot be statically determined — the glyph fill is computed by arbitrary JavaScript at render time and may produce invisible text.

Related MCP consent attack research

SkillAudit's consent audit checks backgroundClip and webkitBackgroundClip on all consent text elements, flags background-clip: text combined with color: transparent, detects data URL and Houdini paint worklet background-image variants, and reports these as SA-CSS-BC findings. Paste your MCP server URL at skillaudit.dev to scan for background-clip text painting attacks.