MCP server CSS stop-color security: SVG gradient stop transparent consent text, stop-opacity zero, CSS vs attribute override, and precise word-position targeting

Published 2026-09-26 — SkillAudit Research

SVG gradients (<linearGradient> and <radialGradient>) are defined by a series of color stops. Each stop specifies a color via stop-color and an optional alpha via stop-opacity. When an SVG <text> element uses a gradient as its fill (fill="url(#gradient)"), the rendered text takes the color of the gradient at each horizontal (or radial) position along the text element's bounding box.

CSS allows both stop-color and stop-opacity to be set as CSS properties on <stop> elements, overriding their SVG presentation attributes via the cascade. This creates an audit surface similar to the CSS fill-opacity override pattern: an SVG gradient's stop attributes may define correct, visible colors, while CSS rules override them to transparent or background-matching values. An audit tool reading SVG attributes finds the original colors; only computing CSS property values reveals the override.

gradient fill → stop-color attack surface: If an SVG consent text element uses a gradient fill, the text color is determined by the gradient stops — not a single fill color. Auditing the fill property of the text element finds url(#gradient-id) (a reference, not a color). To determine the actual rendered color, the auditor must resolve the gradient reference, read all its stops, and compute their colors at each text position. Each stop is a separate attack surface via stop-color and stop-opacity.

Attack findings

CRITICAL
All gradient stops set to transparent — consent text filled with fully transparent gradient; text invisible
An SVG linear gradient fills consent text. All gradient stop colors are overridden via CSS to transparent (alpha=0). The text renders with a gradient that is transparent at every position — effectively invisible. The fill property of the text element is url(#consent-gradient) — a non-null reference. An auditor checking fill for 'none' or 'transparent' finds a URL reference and may proceed to check whether the gradient exists (it does) without verifying the colors of its stops. The element is not display:none, has positive dimensions, and has a non-empty gradient reference — all standard checks pass.
<svg>
  <defs>
    <linearGradient id="consent-gradient">
      <stop offset="0%" stop-color="#333"/>     <!-- attribute: dark color -->
      <stop offset="100%" stop-color="#111"/>   <!-- attribute: dark color -->
    </linearGradient>
  </defs>
  <text fill="url(#consent-gradient)">
    By clicking Accept, you agree to the above terms.
  </text>
</svg>

/* CSS overrides all stops to transparent */
#consent-gradient stop {
  stop-color: transparent;    /* overrides all stop-color attributes */
}

/* el.getAttribute('stop-color'): "#333", "#111" → visible colors → PASS (incorrect)
   getComputedStyle(stop).stopColor: "rgba(0,0,0,0)" → transparent → ATTACK DETECTED
   text fill: url(#consent-gradient) → reference exists → PASS (incorrect)
   Rendered: invisible — gradient is fully transparent */
HIGH
stop-color matching background color at key positions — consent text blends into background
Instead of using transparent stops (which might be caught by an alpha-check), an MCP server sets gradient stop colors to match the dialog background color at positions corresponding to authorization framing text. For a white background, stop-color: #ffffff produces text that is fully visible in the DOM (non-transparent fill color) but visually identical to the background — white text on white background. The attack is more subtle than transparent stops: the color is a valid, non-transparent value, and a contrast-ratio check is needed to identify it, rather than an alpha check.
<linearGradient id="consent-gradient">
  <stop offset="0%"   stop-color="#ffffff"/>  /* matches white background */
  <stop offset="45%"  stop-color="#ffffff"/>  /* still matching background */
  <stop offset="50%"  stop-color="#222222"/>  /* visible: acceptance clause */
  <stop offset="100%" stop-color="#222222"/>  /* visible: binding terms */
</linearGradient>

<!-- Text layout (LTR, left-to-right gradient):
  0–45% = "By clicking Accept, you agree to" → white on white → invisible
  50-100% = "binding arbitration in Delaware." → dark on white → visible

  User sees: "binding arbitration in Delaware." without the consent framing
  Audit: stopColor "#ffffff" is non-transparent → passes alpha check
  Contrast ratio check: #fff on #fff = 1:1 → fails 4.5:1 → ATTACK DETECTED -->
MEDIUM
stop-opacity: 0 separately from stop-color — stop-color audit passes; alpha zeroed by stop-opacity
CSS provides both stop-color and stop-opacity as independent properties. An MCP server sets gradient stops with non-transparent stop-color values (passing color audits) while overriding stop-opacity to 0 via CSS. The resulting gradient is fully transparent — the color exists but has zero alpha. Auditors that check stop-color values find the non-transparent colors. Only checking stop-opacity via computed style reveals the zero alpha. This is the gradient equivalent of the fill-opacity:0 with non-transparent fill color attack.
<linearGradient id="consent-gradient">
  <stop offset="0%"   stop-color="#333"/>    <!-- non-transparent color -->
  <stop offset="100%" stop-color="#111"/>   <!-- non-transparent color -->
</linearGradient>

/* CSS sets stop-opacity to 0 independently */
#consent-gradient stop {
  stop-opacity: 0;    /* zero alpha regardless of stop-color value */
}

/* stop-color audit: "#333", "#111" → non-transparent → PASS (incorrect)
   getComputedStyle(stop).stopOpacity: "0" → zero alpha → ATTACK DETECTED
   stop-color and stop-opacity are separate properties — both must be checked */
MEDIUM
CSS stop-color overrides SVG stop-color attribute — attribute auditors miss CSS-computed values
Like other SVG presentation attributes, stop-color can be set both as a CSS property and as an SVG presentation attribute. The CSS cascade gives author stylesheets higher priority. An MCP server sets stop-color="#222" as an SVG attribute on each <stop> element (visible dark color) while a CSS rule overrides it: #consent-gradient stop { stop-color: transparent; }. A tool that reads stop.getAttribute('stop-color') or parses the SVG element's attribute list finds the dark color. Only getComputedStyle(stop).stopColor reveals the transparent override. This is the same CSS cascade override pattern seen in fill-opacity attribute attacks.
<!-- SVG attribute: visible color -->
<linearGradient id="consent-gradient">
  <stop offset="0%"   stop-color="#2d2d2d"  stop-opacity="1"/>
  <stop offset="100%" stop-color="#1a1a1a"  stop-opacity="1"/>
</linearGradient>

/* CSS cascade override: transparent */
#consent-gradient stop {
  stop-color: rgba(0, 0, 0, 0);    /* overrides attribute "#2d2d2d" */
}

/* stop.getAttribute('stop-color'):   "#2d2d2d"           → PASS (incorrect)
   getComputedStyle(stop).stopColor:  "rgba(0, 0, 0, 0)"  → ATTACK DETECTED
   CSS author stylesheet > SVG presentation attribute in cascade */

Detection

function checkGradientStops(svgRoot) {
  const findings = [];

  /* Find all text elements using gradient fills */
  const textEls = svgRoot.querySelectorAll('text, tspan');
  for (const el of textEls) {
    const fillAttr = el.getAttribute('fill') || getComputedStyle(el).fill || '';
    const gradMatch = fillAttr.match(/url\(#([^)]+)\)/);
    if (!gradMatch) continue;

    const gradId = gradMatch[1];
    const gradient = svgRoot.getElementById(gradId);
    if (!gradient) continue;

    const stops = gradient.querySelectorAll('stop');
    let allTransparent = true;
    let anyBackgroundMatch = false;

    for (const stop of stops) {
      /* Read CSS computed values — not SVG attributes */
      const cs = getComputedStyle(stop);
      const color   = cs.stopColor   || stop.getAttribute('stop-color')   || 'black';
      const opacity = parseFloat(cs.stopOpacity || stop.getAttribute('stop-opacity') || '1');

      /* Check CSS vs attribute mismatch */
      const attrColor = stop.getAttribute('stop-color') || '';
      if (attrColor && cs.stopColor && attrColor !== cs.stopColor) {
        findings.push({
          severity: 'medium', stop,
          issue: `stop-color attribute "${attrColor}" overridden by CSS computed "${cs.stopColor}" — attribute audit sees wrong color`
        });
      }

      /* Check stop-opacity separately */
      const attrOpacity = parseFloat(stop.getAttribute('stop-opacity') || '1');
      const cssOpacity = parseFloat(cs.stopOpacity || '1');
      if (Math.abs(attrOpacity - cssOpacity) > 0.01) {
        findings.push({
          severity: 'medium', stop,
          issue: `stop-opacity attribute="${attrOpacity}" overridden by CSS="${cssOpacity}"`
        });
      }

      /* Check if this stop is transparent */
      const isTransparent = opacity === 0 || color === 'transparent' ||
                            color === 'rgba(0, 0, 0, 0)';
      if (!isTransparent) allTransparent = false;
    }

    if (allTransparent && stops.length > 0) {
      findings.push({
        severity: 'critical', el,
        issue: `SVG text uses gradient fill url(#${gradId}) but all gradient stops are transparent — text invisible`
      });
    }
  }

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
Resolve fill="url(#...)" references and audit gradient stops when consent text uses gradient fillsA gradient fill URL reference passes simple fill-is-transparent checks; only resolving the reference and checking stop colors reveals invisible gradient attacks
Read gradient stop colors via getComputedStyle(stop).stopColor, not via getAttribute('stop-color')CSS cascade overrides SVG presentation attributes; computed style reflects the true rendering value after all CSS rules are applied
Check stop-opacity independently from stop-colorA non-transparent stop-color with zero stop-opacity renders as transparent; both properties must be checked to determine effective alpha
Apply contrast-ratio checks to gradient stop colors against the page backgroundBackground-matching stop colors are non-transparent but produce contrast ratio 1:1 — only contrast measurement catches camouflage attacks that pass alpha checks

SkillAudit resolves gradient fill references on SVG consent text and audits all gradient stops for transparent, background-matching, and CSS-overridden stop colors. Run a free audit on any MCP server GitHub URL to detect SVG gradient stop manipulation and the full consent rendering attack surface.