MCP server SVG pattern security

The SVG <pattern> element defines a tiling brush applied to other SVG elements as their fill or stroke. When applied as fill to a consent text element, the pattern tiles its content across every glyph pixel. A pattern whose tiles are the same color as the page background renders the text as invisible while getComputedStyle(el).fill returns a URL reference — not a transparent color — passing most consent auditors' fill-transparency checks.

Finding SA-PAT-001: near-white pattern tile makes consent text invisible against white background

CriticalA <pattern> element defines a single tile containing a rectangle filled with fill="#fefefe" — a near-white color perceptually indistinguishable from a white background. The pattern is applied as the fill of a consent text element. Each character glyph is rendered in #fefefe against a #ffffff background — invisible to human vision. getComputedStyle(el).fill returns url(#near-white-pattern) — not transparent, not none, not rgba(0,0,0,0). Standard fill-color checks pass; only following the URL reference and checking the tile's fill color reveals the attack.
<defs>
  <pattern id="near-white-pattern" x="0" y="0" width="10" height="10"
           patternUnits="userSpaceOnUse">
    <rect width="10" height="10" fill="#fefefe"/>  <!-- near-white -->
  </pattern>
</defs>

<text fill="url(#near-white-pattern)">
  I authorize access to my calendar, contacts, and email.
</text>

Detection gap: getComputedStyle(el).fill returns url(#near-white-pattern). A check for fill !== 'none' and fill !== 'transparent' passes. A check for fill !== 'rgba(0,0,0,0)' passes. Only pattern URL detection followed by tile fill color analysis catches this attack. The color #fefefe is 254/255 on each channel — a 0.4% difference from white that is invisible at display resolution.

Finding SA-PAT-002: animated patternTransform repositions visible region off glyph pixels

HighA pattern tile contains two regions: a small dark-colored stripe (1px wide) and a large near-white region (9px wide). At load time, the tile is aligned so the dark stripe lands on the glyph edges, making text appear styled but readable. A SMIL animateTransform on the patternTransform attribute shifts the tile position by exactly 1px at button mousedown, moving the dark stripe off the glyph edges and covering all glyph pixels with the near-white region. The text is invisible at the interaction moment.
<defs>
  <pattern id="shift-pattern" x="0" y="0" width="10" height="1"
           patternUnits="userSpaceOnUse">
    <rect width="1" height="1" fill="#333"/>   <!-- 1px dark stripe -->
    <rect x="1" width="9" height="1" fill="#fefefe"/>  <!-- 9px near-white -->
    <animateTransform attributeName="patternTransform" type="translate"
      from="0 0" to="1 0"
      begin="submitBtn.mousedown" dur="indefinite" fill="freeze"/>
  </pattern>
</defs>

Finding SA-PAT-003: patternContentUnits=objectBoundingBox with full-coverage tile

MediumWith patternContentUnits="objectBoundingBox", the pattern tile contents are specified in object-bounding-box coordinates (0–1 range). A tile containing a rectangle with width="1" height="1" in object-bounding-box units covers the entire element — creating a single non-tiling fill equivalent to a solid color. Combined with a near-white fill color, this is structurally equivalent to setting fill="#fefefe" but expressed as a pattern reference, evading fill-color checks. The pattern tile geometry requires evaluation in the correct coordinate system to detect the full-coverage attack.
<defs>
  <pattern id="full-cover" patternContentUnits="objectBoundingBox"
           width="1" height="1">
    <!-- In objectBoundingBox: 0–1 = full element extent -->
    <rect x="0" y="0" width="1" height="1" fill="#fefefe"/>
  </pattern>
</defs>

<text fill="url(#full-cover)">Consent text...</text>

Finding SA-PAT-004: sub-glyph micro-tile pattern creates gray blur

MediumA pattern tile of 1×1 device pixels alternating black and white renders as a uniform 50% gray at display resolution due to sub-pixel antialiasing and display technology. Text rendered with this fill appears as gray noise rather than readable characters. The pattern fill is not transparent, not white, not black — each alternating pixel is a valid non-transparent color. Standard fill checks pass. The attack relies on display-resolution perception limits, not DOM property manipulation. Canvas pixel sampling of the rendered text area reveals the gray-noise rendering.
<defs>
  <pattern id="noise-pattern" x="0" y="0" width="2" height="2"
           patternUnits="userSpaceOnUse">
    <!-- Checkerboard at 1px: renders as uniform gray at display -->
    <rect width="1" height="1" fill="black"/>
    <rect x="1" width="1" height="1" fill="white"/>
    <rect y="1" width="1" height="1" fill="white"/>
    <rect x="1" y="1" width="1" height="1" fill="black"/>
  </pattern>
</defs>

<text fill="url(#noise-pattern)">Consent text...</text>

Detection: following pattern fill references and checking tile colors

function checkSVGPatternFills(consentEl) {
  const risks = [];
  const fillAttr = consentEl.getAttribute('fill')
                || getComputedStyle(consentEl).fill;

  if (!fillAttr || !fillAttr.includes('url(')) return risks;

  const idMatch = fillAttr.match(/url\(#([^)]+)\)/);
  if (!idMatch) return risks;

  const patternEl = document.getElementById(idMatch[1]);
  if (!patternEl || patternEl.tagName.toLowerCase() !== 'pattern') return risks;

  // Check tile shapes for near-background fill colors
  const tileShapes = patternEl.querySelectorAll('rect, circle, ellipse, path');
  let allNearWhite = true;
  let tileCount = 0;

  for (const shape of tileShapes) {
    tileCount++;
    const computedFill = getComputedStyle(shape).fill || shape.getAttribute('fill') || '';
    if (!isNearBackground(computedFill)) {
      allNearWhite = false;
    }

    // Check for SMIL animation on patternTransform
    const animEls = shape.querySelectorAll('animateTransform[attributeName="patternTransform"]');
    for (const anim of animEls) {
      const begin = anim.getAttribute('begin') || '';
      if (/click|mousedown|focus/.test(begin)) {
        risks.push({ finding: 'SA-PAT-002', severity: 'high',
          note: 'patternTransform animated at interaction — may shift visible region off glyphs' });
      }
    }
  }

  if (tileCount > 0 && allNearWhite) {
    risks.push({ finding: 'SA-PAT-001', severity: 'critical',
      note: 'All pattern tile fills are near-background color — text rendered invisible' });
  }

  // Check for sub-pixel tile size (SA-PAT-004)
  const tileW = parseFloat(patternEl.getAttribute('width') || '10');
  const tileH = parseFloat(patternEl.getAttribute('height') || '10');
  if (tileW <= 2 && tileH <= 2 && tileCount > 1) {
    risks.push({ finding: 'SA-PAT-004', severity: 'medium',
      note: 'Sub-pixel pattern tile — may render as gray noise at display resolution' });
  }

  return risks;
}

function isNearBackground(colorStr) {
  if (/^(white|#fff|#ffffff)$/i.test(colorStr)) return true;
  const m = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!m) return false;
  return parseInt(m[1]) > 240 && parseInt(m[2]) > 240 && parseInt(m[3]) > 240;
}

Remediation

ControlHow it helps
Detect any fill or stroke attribute on consent text elements that contains a url() reference; follow the reference to the <pattern> element; compute the luminance of all tile shapes via getComputedStyle; flag if all tiles are near-background luminance (> 0.9) SA-PAT-001 requires following the fill URL reference. getComputedStyle(el).fill returns the URL — not the color — passing all standard non-transparent checks. Pattern fill URL detection must trigger pattern tile analysis.
For pattern fills on consent elements, check for animate and animateTransform children of the pattern element with begin= referencing interaction events; evaluate the post-animation tile positions for coverage against the consent glyph bounding box SA-PAT-002 is a SMIL timing attack specific to patternTransform. The safe tile alignment at load time does not guarantee safe alignment at interaction time. Only animate child enumeration with begin= inspection reveals the timing attack.
Do not apply SVG pattern fills to consent text elements; use solid color fills with explicit color values for all consent text Preventive: pattern fills on consent text have no legitimate use case. An unconditional flag on any pattern fill URL reference on a consent text element eliminates all four vectors without requiring tile geometry analysis.

SkillAudit follows SVG pattern fill URL references, computes tile fill luminance via computed styles, detects patternTransform animation at interaction, and flags sub-pixel tile patterns that render as gray noise. Run a free audit on your MCP server GitHub URL.