MCP server CSS stop-opacity security: SVG gradient stop zero alpha, near-zero opacity, word-position targeting, and CSS vs attribute override

Published 2026-09-26 — SkillAudit Research

SVG gradient stops (<stop>) have two properties that together define their rendered color: stop-color (the hue and saturation) and stop-opacity (the alpha channel). These are independent CSS properties. An auditor that checks stop-color for transparency but overlooks stop-opacity may conclude that gradient stops are visible when every stop has zero alpha via stop-opacity.

When an SVG <text> element uses a gradient fill (fill="url(#gradient)"), the text color at each horizontal position is determined by the interpolated stop color and stop opacity at that position. Both properties must be computed via getComputedStyle(stop).stopColor and getComputedStyle(stop).stopOpacity — reading SVG attributes directly misses CSS cascade overrides. This is the alpha-channel complement to the stop-color targeting attack.

Two-channel attack surface: SVG gradient stop transparency has two independent CSS channels: stop-color (controls RGBA color value) and stop-opacity (controls alpha multiplied with the color's alpha). An MCP server can set non-transparent stop-color values to pass color audits while zeroing stop-opacity to produce a transparent rendering result. Both must be checked via computed style to determine effective visibility.

Attack findings

HIGH
All gradient stops at stop-opacity: 0 — consent text rendered via gradient fill is fully transparent despite non-transparent stop-color values
A linear gradient fills consent text. All gradient stop stop-color values are non-transparent dark colors (passing color audits). CSS overrides all stop-opacity values to 0 — zeroing the alpha independently of the color channel. The rendered text is fully transparent. A tool checking stop-color for transparent values finds dark, non-transparent colors and reports the gradient as visible. Only reading getComputedStyle(stop).stopOpacity reveals zero alpha. The element has positive dimensions, a valid gradient fill reference, and non-transparent stop colors — all standard visibility checks pass.
<linearGradient id="consent-gradient">
  <stop offset="0%"   stop-color="#2d2d2d"/>   <!-- dark, non-transparent -->
  <stop offset="100%" stop-color="#1a1a1a"/>   <!-- dark, non-transparent -->
</linearGradient>

/* CSS zeros stop-opacity on all stops */
#consent-gradient stop {
  stop-opacity: 0;    /* independent of stop-color; zeros the alpha channel */
}

/* getAttribute('stop-color'): "#2d2d2d", "#1a1a1a" → visible colors → PASS (incorrect)
   getComputedStyle(stop).stopOpacity: "0" → zero alpha → ATTACK DETECTED
   Rendered text: fully transparent — stop-color has non-null value but stop-opacity = 0 */
HIGH
stop-opacity: 0.01 — passes non-zero check; renders at 0.25% alpha; imperceptibly faint against any background
Setting CSS stop-opacity to 0.01 produces gradient stops that pass a basic non-zero opacity check (stopOpacity > 0 → continue) but renders consent text at 1% alpha. Against a white background, the contrast ratio is approximately 1.003:1, far below the 3:1 minimum for large text. The text is functionally invisible — indistinguishable from the background at any display density. The attack is harder to detect than zero because automated checks that threshold on > 0 pass it through; only a contrast-ratio measurement against the rendered background color catches it.
/* Passes simple non-zero check */
#consent-gradient stop {
  stop-opacity: 0.01;    /* passes: stopOpacity (0.01) > 0 → TRUE */
}

/* Effect:
   stop-color: #2d2d2d (dark gray)
   stop-opacity: 0.01 → effective alpha = 1% = rgba(45, 45, 45, 0.01)
   Against white background: contrast ratio ≈ 1.003:1
   WCAG AA minimum: 3:1 for large text, 4.5:1 for normal text → FAIL

   Detection threshold must be: stopOpacity >= MIN_READABLE_OPACITY
   where MIN_READABLE_OPACITY is determined by contrast calculation, not > 0 */
MEDIUM
Position-targeted stop-opacity: authorization framing at 0% alpha, acceptance clause at full opacity — consent framing hidden, binding clause visible
Gradient stop positions can be used to target specific regions of the consent text for opacity manipulation. Setting stop-opacity: 0 for stops in the 0–45% gradient position range (covering authorization framing text) while using full opacity in the 50–100% range (covering the acceptance clause) makes only the framing invisible. The user sees the binding acceptance clause without the context that explains what they are accepting. This mirrors the gradient position word-targeting technique used in stop-color attacks but via the alpha channel rather than hue matching.
<linearGradient id="consent-gradient">
  <stop offset="0%"   stop-color="#333" stop-opacity="0"/>  /* framing: hidden */
  <stop offset="44%"  stop-color="#333" stop-opacity="0"/>  /* framing: hidden */
  <stop offset="46%"  stop-color="#333" stop-opacity="1"/>  /* clause: visible */
  <stop offset="100%" stop-color="#333" stop-opacity="1"/>  /* clause: visible */
</linearGradient>

<!-- Text content (LTR, gradient left-to-right):
  0–44%: "By clicking Accept you agree to"  → stop-opacity 0 → invisible
  46–100%: "binding arbitration in Delaware" → stop-opacity 1 → dark, readable

  User sees only the binding clause, not the consent framing
  Detection: check stop-opacity values at each position; flag near-zero in any range -->
MEDIUM
CSS stop-opacity overrides SVG stop-opacity attribute — attribute reads return visible opacity; computed style reveals zero
CSS author stylesheets have higher priority than SVG presentation attributes in the cascade. An MCP server sets stop-opacity="1" as an SVG attribute on all gradient stops (full opacity) while a CSS rule overrides it: #consent-gradient stop { stop-opacity: 0; }. A tool reading stop.getAttribute('stop-opacity') or parsing SVG attributes finds "1". Only getComputedStyle(stop).stopOpacity reveals the CSS override value of "0". This pattern is equivalent to the CSS cascade override attacks documented for fill-opacity and stop-color.
<!-- SVG attribute: full opacity -->
<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: zero opacity */
#consent-gradient stop {
  stop-opacity: 0;    /* overrides attribute stop-opacity="1" */
}

/* stop.getAttribute('stop-opacity'):        "1"   → full opacity → PASS (incorrect)
   getComputedStyle(stop).stopOpacity:       "0"   → zero alpha   → ATTACK DETECTED
   CSS author > SVG presentation attribute in cascade; attribute audit is insufficient */

Detection

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

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

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

    const stops = gradient.querySelectorAll('stop');
    let allTransparentViaOpacity = true;

    for (const stop of stops) {
      /* Must use getComputedStyle — attribute may be overridden by CSS */
      const cs = getComputedStyle(stop);
      const cssOpacity = parseFloat(cs.stopOpacity ?? '1');
      const attrOpacity = parseFloat(stop.getAttribute('stop-opacity') ?? '1');

      /* Check for CSS override of attribute */
      if (Math.abs(cssOpacity - attrOpacity) > 0.01) {
        findings.push({
          severity: 'medium', stop,
          issue: `stop-opacity attribute="${attrOpacity}" overridden by CSS computed="${cssOpacity}"`
        });
      }

      /* Check near-zero: passes > 0 but renders near-invisible */
      if (cssOpacity > 0 && cssOpacity < 0.05) {
        findings.push({
          severity: 'high', stop,
          issue: `stop-opacity="${cssOpacity}" passes >0 check but renders at ${(cssOpacity * 100).toFixed(1)}% alpha — effectively invisible`
        });
      }

      if (cssOpacity > 0.05) allTransparentViaOpacity = false;
    }

    if (allTransparentViaOpacity && stops.length > 0) {
      findings.push({
        severity: 'high', el,
        issue: `All gradient stops have near-zero stop-opacity — consent text invisible despite non-transparent stop-color values`
      });
    }

    /* Check position-targeted opacity: compare opacity across stop positions */
    const opacityByPosition = Array.from(stops).map(s => ({
      offset: parseFloat(s.getAttribute('offset') ?? '0'),
      opacity: parseFloat(getComputedStyle(s).stopOpacity ?? '1')
    }));
    const lowPositions  = opacityByPosition.filter(s => s.offset < 0.5 && s.opacity < 0.1);
    const highPositions = opacityByPosition.filter(s => s.offset >= 0.5 && s.opacity > 0.5);
    if (lowPositions.length > 0 && highPositions.length > 0) {
      findings.push({
        severity: 'medium', gradient,
        issue: 'Gradient has near-zero stop-opacity in first half and visible opacity in second half — authorization framing may be hidden while acceptance clause is visible'
      });
    }
  }

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
Read stop-opacity via getComputedStyle(stop).stopOpacity, not via getAttribute('stop-opacity')CSS cascade overrides SVG presentation attributes; computed style reflects the actual alpha value used in rendering
Treat stop-opacity < 0.05 (not just stop-opacity === 0) as a visibility failure for consent text gradient stopsValues like 0.01–0.04 pass zero-check thresholds but produce contrast ratios below 1.05:1, effectively invisible at all font sizes
Check both stop-color and stop-opacity independently — a non-transparent stop-color with zero stop-opacity renders transparentThe two properties are independent CSS channels; auditing only one channel misses attacks that exploit the other
Check stop-opacity values across all gradient stop positions, not just the first or last stopPosition-targeted opacity attacks zero alpha only in the portion of the gradient that covers authorization framing text

SkillAudit computes stop-opacity values via CSS cascade resolution on all gradient stops used by consent text, checking for zero, near-zero, and position-targeted opacity attacks independently of the stop-color channel. Run a free audit on any MCP server GitHub URL to detect SVG gradient stop opacity manipulation across the full consent rendering attack surface.