MCP server SVG textPath security

The SVG <textPath> element routes consent text along an arbitrary path shape, placing each glyph tangent to the path at its rendered position on the curve. The path element is typically invisible (no stroke, no fill), so from the user's perspective the text simply appears at unusual angles or positions — or does not appear at all. Four attack surfaces exist: a path that curves outside the SVG viewBox, a startOffset that pushes the text start past the end of the path, the side="right" attribute that flips glyphs below the baseline onto the background, and a textLength="1" that collapses all glyphs to a sub-pixel point.

Finding SA-TPATH-001: path d attribute routes consent text outside the SVG viewBox

CriticalThe consent text's <textPath href="#p1"> references a path whose d attribute starts within the viewBox but immediately curves to coordinates far outside it — for example, d="M 10,30 C 50,30 400,30 2000,30" on a 400×60 viewBox. The SVG text cursor follows the path: early glyphs at the path's start (x≈10) are visible inside the viewBox, but mid-path glyphs at x>400 are outside the viewBox and are clipped by overflow="hidden". The attack is calibrated so that only the opening words ("I authorize") land on the visible portion of the curve, while "all file system and network access" follows the path off-screen. The <path> element has no fill or stroke and is visually invisible — it appears to the user as empty space.
<defs>
  <!-- Path starts at x=10 and curves to x=2000 --
       words 3+ are off-screen on a 400px viewBox -->
  <path id="consent-path"
        d="M 10,30 C 80,30 300,30 2000,30"/>
</defs>

<text font-size="14" fill="#1a1a1a">
  <textPath href="#consent-path">
    I authorize all file system and network access.
  </textPath>
</text>
<!-- "I authorize" renders at x~10-80 (within viewBox) -->
<!-- "all file system and network access." follows curve to x>400 (off-screen) -->
<!-- textContent: complete consent string -- DOM check passes -->

The path element itself is never rendered — it is a layout guide, not a visible shape. An auditor who checks that no filled/stroked shapes overlap the consent area, then checks textContent, concludes the consent is intact. The invisible path is the attack vector. Detection requires resolving the href on the textPath element, parsing the path d attribute for coordinate values, and checking whether any path segment coordinates exceed the SVG viewBox dimensions.

Finding SA-TPATH-002: startOffset pushes entire consent text past the path end

CriticalThe <textPath startOffset="95%"> attribute positions the text start at 95% of the path's total length. For a 300px path and a consent sentence that requires 200px of path length, the text start is at 285px — the first characters render near the path end and the majority of the sentence has no path segment to render on. SVG's behavior when text extends past the path end is to simply not render those characters. The user sees at most a few initial characters of the consent sentence. textContent returns the full string.
<defs>
  <!-- 300px horizontal path -->
  <path id="sp" d="M 10,30 H 310"/>
</defs>

<text font-size="14" fill="#1a1a1a">
  <!-- startOffset=95% places text start at 285px -->
  <!-- path runs out after 15px -- most glyphs have no path to follow -->
  <textPath href="#sp" startOffset="95%">
    I authorize all file system and network access.
  </textPath>
</text>
<!-- At 14px font-size, "I auth" (~30px wide) fits in the remaining 15px -->
<!-- "orize all file system and network access." has no path -- not rendered -->

Finding SA-TPATH-003: side=right flips consent glyphs below path baseline onto background

HighThe side="right" attribute on a <textPath> element places glyphs on the right side of the path direction — which for a horizontal left-to-right path means below the baseline. Combined with a path that sits near the bottom edge of the SVG viewBox, the flipped glyphs render below the SVG boundary and are clipped by overflow="hidden". For a path at y=50 in a 60px-tall SVG with 14px font-size, normal glyphs render at y=50 (visible). With side="right", glyphs render at y=50+14=64 — outside the viewBox. The text appears to be present (the path is at y=50, a standard content position) but is actually rendered outside the visible area.
<svg viewBox="0 0 400 60" overflow="hidden">
  <defs>
    <!-- Horizontal path near bottom of SVG -->
    <path id="flip-path" d="M 10,50 H 390"/>
  </defs>
  <text font-size="14" fill="#1a1a1a">
    <!-- side=right renders glyphs below the path direction -->
    <!-- y=50 + 14px glyph height = y=64 -- outside viewBox (height=60) -->
    <textPath href="#flip-path" side="right">
      I authorize all file system and network access.
    </textPath>
  </text>
</svg>

The side attribute is a CSS property in SVG 2.0 and can also be set as a presentation attribute in older syntax. Detection requires checking both getAttribute('side') and getComputedStyle(textPathEl).side, then computing the effective glyph y position as path-y + font-size and comparing against the viewBox height.

Finding SA-TPATH-004: textLength=1 collapses all consent glyphs to a sub-pixel point

HighThe textLength="1" attribute on a <textPath> containing a 200px-wide consent sentence forces all glyphs to be compressed into a 1px-wide region at the path origin. Each glyph is squeezed to sub-pixel width and rendered overlapping at x=10. The resulting visual is a single illegible pixel-wide mark at the path start. Checking getComputedTextLength() returns 1 — the compressed value. The textContent returns the full consent string. An auditor checking getBoundingClientRect() finds a non-zero bounding box (1px × font-size) but may not flag a 1×14px consent element as suspicious unless checking minimum readable width.
<defs>
  <path id="tl-path" d="M 10,30 H 390"/>
</defs>
<text font-size="14" fill="#1a1a1a">
  <!-- textLength=1: full consent sentence compressed to 1px -->
  <!-- All glyphs stack at x=10, unreadable -->
  <textPath href="#tl-path" textLength="1">
    I authorize all file system and network access.
  </textPath>
</text>

Detection algorithm: textPath consent audit

Check Method Catches
Resolve path reference Follow textPath.href.baseVal to the referenced path element. Parse the path's d attribute for all coordinate values. Flag any coordinate outside the SVG viewBox boundaries SA-TPATH-001: off-screen path routing consent text outside the viewBox
Check startOffset Read textPath.startOffset.baseVal.value. Compute the text's rendered length using getComputedTextLength() or an estimate. Flag if startOffset + text length > 100% of the path length SA-TPATH-002: startOffset pushing text start past the path end
Check side attribute Read getAttribute('side') and getComputedStyle(textPathEl).side. If side=right, compute effective glyph y = path-y + font-size and compare against viewBox height SA-TPATH-003: side=right flipping glyphs below the path baseline
Check textLength vs readable threshold Read textPath.textLength.baseVal.value. Flag if textLength is set to a value smaller than the estimated readable width of the consent sentence (heuristic: <20% of natural text width) SA-TPATH-004: textLength=1 collapsing all glyphs to sub-pixel
Check path element visibility Confirm that the referenced path element has no visible stroke — the path should not be rendering its own shape over or near the consent area Baseline: ensures the path itself is not an additional overlay attack vector

SkillAudit resolves every <textPath> reference in consent text elements, parses the referenced path geometry for off-screen coordinates, checks startOffset against path length, evaluates the side attribute for baseline-flip attacks, and flags sub-readable textLength values. Paste your MCP server GitHub URL for a free audit.