MCP server SVG linearGradient security

The SVG <linearGradient> element defines a gradient paint server that can be referenced as a fill or stroke value on SVG shapes and text. Unlike CSS linear-gradient() which produces an image-type background, the SVG linearGradient is an element-level paint server — consent text can use it directly as a fill via fill="url(#grad)". This distinction matters: standard fill-color checks that look for rgba(0,0,0,0) or transparent receive url(#grad) from getComputedStyle instead of a color value, and return a false pass.

Finding SA-LGRAD-001: near-white gradient stops render consent text imperceptible

CriticalA <linearGradient> with stop-color="#fefefe" at offset 0% and stop-color="#fdfdfd" at offset 100% is used as the fill of the consent text element. Both stops are less than 1% luminance difference from white (#ffffff). Glyph pixels are rendered in #fefefe on a white background — the text is physically present and has non-zero alpha, but the contrast ratio is 1.005:1, far below the WCAG 1.5:1 minimum. getComputedStyle(el).fill returns "url(#near-white-grad)" — not a color value. Checks like fill === 'transparent' or fill.includes('rgba(0,0,0,0)') pass. The text is effectively invisible.
<defs>
  <linearGradient id="near-white-grad" x1="0%" y1="0%" x2="100%" y2="0%">
    <!-- Both stops differ from white by <1% luminance -- imperceptible -->
    <stop offset="0%" stop-color="#fefefe"/>
    <stop offset="100%" stop-color="#fdfdfd"/>
  </linearGradient>
</defs>
<text x="10" y="30" font-size="14" fill="url(#near-white-grad)">
  I authorize all requested MCP server permissions
</text>

Detection requires following the fill URL reference to the gradient element, iterating all <stop> children, reading their stop-color and stop-opacity values, and computing the luminance contrast of each stop against the expected background color. A gradient where all stops have contrast below 1.5:1 against the page background is a Critical finding regardless of whether the fill value itself appears transparent.

Finding SA-LGRAD-002: gradientUnits=userSpaceOnUse positions dark stop outside element boundary

CriticalA <linearGradient> with gradientUnits="userSpaceOnUse" defines its start and end points in the SVG user coordinate system. The gradient's x1 is set to a position 50px to the right of the consent text element's right edge, so the dark stop at 0% falls outside the element's bounding box. The x2 is far to the right at 1000. At the consent element's position, the gradient has already progressed past the dark region — the text fills with near-white. The gradient parameters look like reasonable absolute coordinates, not an obvious attack.
<!-- Consent text element is at x=10, width ~300px, so right edge ≈ x=310 -->
<defs>
  <!-- gradientUnits=userSpaceOnUse: x1/x2 are in SVG document coordinates -->
  <linearGradient id="shifted-grad"
                  gradientUnits="userSpaceOnUse"
                  x1="360" y1="0" x2="1000" y2="0">
    <!-- Dark stop is at x=360 — 50px right of the consent text's right edge -->
    <stop offset="0%" stop-color="#1a1a1a"/>
    <!-- Near-white stop is at x=1000 -->
    <stop offset="100%" stop-color="#fefefe"/>
  </linearGradient>
</defs>
<!-- Text is at x=10–310; gradient is #fefefe across the entire element width -->
<text x="10" y="30" font-size="14" fill="url(#shifted-grad)">
  By clicking Agree you authorize all requested permissions
</text>

With gradientUnits="userSpaceOnUse", the gradient's effective color at any position inside the consent text is determined by interpolating between the stops at the absolute document coordinates x1 and x2, not relative to the element's bounding box. An auditor who sees stop-color="#1a1a1a" at offset 0% concludes the gradient starts dark — without checking whether x1 falls inside the consent element's bounding box, the auditor misses that the element only receives the near-white end of the gradient.

Finding SA-LGRAD-003: SMIL animate transitions stop-color to white at button interaction

HighA <linearGradient> initially has a legitimate dark stop color. An <animate> child element on the <stop> transitions the stop-color attribute from #1a1a1a to #ffffff at begin="agreeBtn.click" with fill="freeze". At the moment the user clicks the Agree button, the consent text fill gradient transitions from dark to white — the text disappears exactly when the consent action is recorded. Static analysis reads the initial stop-color="#1a1a1a" and concludes the gradient is legitimate. The attack value is in the SMIL to attribute, not in the stop itself.
<defs>
  <linearGradient id="timing-grad" x1="0%" y1="0%" x2="100%" y2="0%">
    <stop offset="0%">
      <!-- Static analysis reads stop-color="#1a1a1a" — looks legitimate -->
      <animate attributeName="stop-color"
               from="#1a1a1a" to="#ffffff"
               begin="agreeBtn.click" dur="0.01s"
               fill="freeze"/>
      <!-- No stop-color attribute here — initial value is black from animate from= -->
    </stop>
    <stop offset="100%" stop-color="#1a1a1a"/>
  </linearGradient>
</defs>
<text x="10" y="30" font-size="14" fill="url(#timing-grad)">
  I authorize all requested permissions
</text>
<rect id="agreeBtn" x="250" y="40" width="80" height="24" fill="#4f46e5"
      class="consent-button"/>

Finding SA-LGRAD-004: CSS custom property overrides stop-color after static analysis

HighA <stop> element uses stop-color="var(--consent-text-color, #1a1a1a)". The SVG source has a safe-looking dark fallback. The MCP server's injected stylesheet sets :root { --consent-text-color: #fefefe; }. getAttribute('stop-color') returns the variable syntax with the fallback — static analysis concludes dark. getComputedStyle(stop).stopColor after full stylesheet load returns the near-white computed value. The attack is cross-file: the SVG and the CSS are individually benign; the combination is the attack.
<!-- In the SVG -->
<defs>
  <linearGradient id="var-grad" x1="0%" y1="0%" x2="100%" y2="0%">
    <!-- getAttribute returns "var(--consent-text-color, #1a1a1a)" -->
    <!-- getComputedStyle returns #fefefe (attacker stylesheet) -->
    <stop offset="0%" stop-color="var(--consent-text-color, #1a1a1a)"/>
    <stop offset="100%" stop-color="var(--consent-text-color, #1a1a1a)"/>
  </linearGradient>
</defs>

<!-- In the MCP server injected stylesheet (separate file) -->
<!-- :root { --consent-text-color: #fefefe; } -->

SVG stop-color supports CSS custom properties since SVG is styled via CSS. Static analysis that reads getAttribute('stop-color') sees the variable syntax and its fallback — concluding safe. Only reading getComputedStyle(stopElement).stopColor after the complete stylesheet cascade is applied reveals the attacker's override. This cross-file pattern distributes the attack signal across two sources, each individually benign.

Detection algorithm: auditing SVG linearGradient consent text fills

Step Action What it catches
1 For any consent text element, check if getComputedStyle(el).fill returns a url(#...) reference. If so, resolve the referenced <linearGradient> element via querySelector Identifies gradient fill usage vs solid colors
2 Iterate all <stop> children. For each stop, read getComputedStyle(stop).stopColor (not getAttribute) and getComputedStyle(stop).stopOpacity SA-LGRAD-004: catches CSS custom property overrides that getAttribute misses
3 Compute the luminance contrast of each stop's effective color against the page background. Flag gradients where all stops have contrast <1.5:1 as Critical SA-LGRAD-001: near-white gradient stops on white background
4 If gradientUnits="userSpaceOnUse", compute the gradient's value at the consent element's bounding box: interpolate stops between x1/y1 and x2/y2 at the element's position. Check effective color at element coordinates, not at gradient axis positions SA-LGRAD-002: dark stop at x1 outside element boundary, near-white at element position
5 Recurse into each <stop> element's children. Flag any <animate> or <set> element targeting stop-color or stop-opacity with a begin value matching an interaction event SA-LGRAD-003: SMIL animate transitioning stop-color to white on button click

SkillAudit follows fill="url(#...)" references to linearGradient elements, reads stop colors via getComputedStyle (not getAttribute), evaluates effective color at the consent element's position under userSpaceOnUse gradients, and recursively inspects SMIL animate children on stop elements. Run a free audit on your MCP server GitHub URL.