MCP server SVG feMorphology security

The SVG <feMorphology> filter primitive applies morphological erosion (operator="erode") or dilation (operator="dilate") to its input image. Erosion shrinks bright foreground regions by eroding their edges inward by the specified radius in pixels: every foreground pixel within radius pixels of a background pixel becomes background. On consent text glyphs, erosion thins stroke widths — at a sufficient radius relative to the font-size and stroke width, glyphs erode to sub-pixel width and disappear entirely at normal screen resolution. Dilation expands foreground regions outward, bloating stroke widths until adjacent character strokes merge into an illegible mass. In both cases the consent text is present in the DOM with correct fill, font-size, and textContent attributes — the filter is the only attack vector.

Finding SA-FEMM-001: operator=erode radius=5 shrinks glyph strokes to sub-pixel width

CriticalA <feMorphology operator="erode" radius="5"> primitive applied to consent text at font-size="14" erodes all glyph pixels. A 14px sans-serif font typically has stroke widths of approximately 1–2px for thin strokes and up to 3px for thick strokes. An erosion radius of 5px removes any foreground pixel within 5 pixels of a background pixel — this exceeds the stroke width for all glyph pixels in a 14px font, reducing the entire glyph raster to zero foreground pixels. The rendered consent text is blank. The text element's font-size attribute reads 14, its fill reads a dark color, its textContent contains the authorization text, but the filter has eroded all visible glyph pixels to nothing.
<defs>
  <filter id="erodeConsent">
    <!-- radius=5 exceeds stroke width for 14px font — all glyphs erode to 0 pixels -->
    <feMorphology operator="erode" radius="5"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#1f2937"
      filter="url(#erodeConsent)">
  I authorize all requested MCP server permissions including
  file system access and network transmission of captured data.
</text>

The legibility threshold for feMorphology erode is font-size and stroke-width dependent. Detection requires computing the effective post-erosion stroke width: for a given font-size and typeface, estimate the minimum glyph stroke width (approximately font-size × 0.07 for thin strokes in a normal-weight sans-serif), then compare against radius. If radius ≥ min-stroke-width / 2, glyph strokes will be partially or fully eroded. SkillAudit flags operator="erode" with radius ≥ font-size × 0.04 on consent elements as Critical.

Finding SA-FEMM-002: operator=dilate radius=3 merges adjacent character strokes into unreadable blob

HighA <feMorphology operator="dilate" radius="3"> primitive applied to 14px consent text expands every foreground pixel outward by 3 pixels. At 14px, character spacing is approximately 8–10px between adjacent glyph bounding boxes. A dilation radius of 3px expands each glyph stroke by 3px in all directions, closing approximately 6px of the 8–10px inter-glyph gap. Adjacent character strokes merge into connected blobs. The resulting render is an illegible mass of ink rather than readable text: characters are no longer individually distinguishable, and the authorization scope is unreadable. Unlike erosion (which removes pixels), dilation preserves glyph presence — a pixel-presence check would report "text is rendered" but legibility is destroyed.
<defs>
  <filter id="dilateConsent">
    <!-- radius=3 at 14px font-size: inter-glyph gap ~8px; dilation closes ~6px of that gap -->
    <!-- adjacent character strokes merge into an illegible connected blob -->
    <feMorphology operator="dilate" radius="3"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#1f2937"
      filter="url(#dilateConsent)">
  Authorize MCP server credential and filesystem access
</text>

Dilation-based illegibility is distinct from erasure: a pixel presence check reports ink on screen. Detection must evaluate the ratio of dilation radius to font-size. Legibility degrades significantly when dilation radius exceeds font-size × 0.15 (approximately 2.1px at 14px), as inter-glyph gaps begin to close. SkillAudit flags operator="dilate" with radius ≥ font-size × 0.15 on consent elements as High, independent of whether strokes fully merge.

Finding SA-FEMM-003: SMIL animate on radius from 0 to 6 at agreeBtn.focus triggers full erasure

CriticalAn <animate> child on <feMorphology> targets attributeName="radius", animating from 0 (no erosion — consent text fully visible) to 6 (full erasure at 14px font-size) triggered by agreeBtn.focus with fill="freeze". At page load, radius="0" means feMorphology is a no-op: the filter passes SourceGraphic through unchanged. The consent text is fully visible. When the user focuses the Agree button, the erosion radius jumps to 6px, eroding all glyph strokes to zero at 14px. A static DOM scan reads radius="0" and concludes feMorphology has no effect: the attack value appears only in the animate's to attribute.
<defs>
  <filter id="triggerErode">
    <feMorphology operator="erode" radius="0">
      <!-- animate radius from 0 (no-op) to 6 (full erasure) at agreeBtn.focus -->
      <animate attributeName="radius"
               from="0" to="6"
               begin="agreeBtn.focus"
               dur="50ms"
               fill="freeze"/>
    </feMorphology>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111827"
      filter="url(#triggerErode)">
  By clicking Agree you authorize this MCP server to read all files
  and transmit data to external endpoints including credentials.
</text>

<rect id="agreeBtn" x="250" y="65" width="130" height="36"
      fill="#2563eb" rx="6"/>
<text x="315" y="89" font-size="14" fill="white"
      text-anchor="middle" pointer-events="none">Agree</text>

The from="0" baseline on the radius animate makes the static DOM state benign: radius=0 means feMorphology does nothing. The attack radius appears only in the animate to attribute. Detection requires: (1) scanning for <animate> children on <feMorphology> elements in consent-element filters; (2) reading the to attribute; (3) computing the effective post-trigger stroke width at the given font-size; (4) flagging if erode radius exceeds the computed minimum stroke width threshold; (5) confirming interaction-event correlation on begin.

Finding SA-FEMM-004: feMorphology erode combined with light fill color — invisible thin strokes

HighA moderate erosion radius (e.g., radius="2") alone may not fully erase 14px glyphs — thin strokes survive at 0.5–1px width. Combined with a light gray or near-white fill color (e.g., fill="#e5e7eb"), the sub-pixel thin glyph remnants are already near-invisible on a white background. The combined attack: feMorphology erode radius=2 thins strokes to 1px, and the light fill produces 1px strokes at approximately 1.2:1 WCAG contrast on a white background — below the 3:1 minimum for normal text, and far below the 4.5:1 minimum for small text. Neither factor alone might cross the detection threshold: radius=2 does not fully erase and light fill alone is borderline. The compound effect is complete illegibility.
<defs>
  <filter id="erodeLight">
    <!-- radius=2 thins strokes to ~1px without fully erasing at 14px -->
    <feMorphology operator="erode" radius="2"/>
  </filter>
</defs>

<!-- fill="#e5e7eb" (Tailwind gray-200) + erode radius=2 = WCAG contrast ~1.2:1 -->
<text x="20" y="50" font-size="14" fill="#e5e7eb"
      filter="url(#erodeLight)">
  Grant this MCP server access to credentials, keys, and private documents
</text>

Detection of compound attacks requires evaluating feMorphology radius and fill color together. SkillAudit computes the predicted post-erosion pixel coverage at the given font-size and resolves the fill color's contrast against the effective background. If the combined predicted legibility (stroke-coverage × contrast ratio) falls below the legibility threshold, the compound finding is flagged High — regardless of whether either factor alone would cross a threshold. The erosion radius threshold is lowered when fill color contrast is already degraded: radius ≥ font-size × 0.10 is flagged when fill contrast is below 3:1.

Detection algorithm: feMorphology filter-context attacks on consent subtrees

Step Action What it catches
1 Collect all <feMorphology> primitives inside filters that apply to consent text elements. For each, read operator and radius (or radiusX / radiusY for two-value radius) Establishes the morphology type and radius for downstream analysis
2 For operator="erode": compute the effective minimum stroke width at the element's resolved font-size. Estimate minimum stroke width as font-size × 0.07 for normal-weight sans-serif. Flag Critical if radius ≥ min-stroke-width / 2 (partial erasure threshold). Flag Critical if radius ≥ min-stroke-width (full erasure) SA-FEMM-001: erode stroke thinning to sub-pixel width and full erasure
3 For operator="dilate": estimate inter-glyph gap as approximately font-size × 0.5. Flag High if radius ≥ (inter-glyph-gap / 2) × 0.8 (gap-closing threshold producing illegibility) SA-FEMM-002: dilate character merging into unreadable blob
4 Check for <animate> children targeting attributeName="radius". Read animate to value. Apply erode/dilate thresholds against the to value. Flag Critical if post-trigger state crosses erode or dilate threshold and begin is consent-interaction-correlated SA-FEMM-003: SMIL animate on radius triggered at interaction
5 If erode radius is moderate (below full-erasure threshold), resolve the consent element's fill color WCAG contrast. Compute compound legibility score: (post-erosion stroke coverage) × (fill contrast). Flag High if compound score indicates illegibility below WCAG 3:1 equivalent SA-FEMM-004: erode combined with light fill — compound illegibility

SkillAudit evaluates feMorphology radius against font-size-dependent stroke width thresholds, applies compound scoring for moderate erosion combined with light fill color, and scans for SMIL animate children on feMorphology in consent element filters. Run a free audit on your MCP server's GitHub URL.