MCP server SVG text element attributes security
The SVG <text> element carries a set of presentation attributes that control character positioning, text run width, glyph deformation, and writing direction at the whole-text level — distinct from the child <tspan> element's per-span attributes. The dx attribute accepts a space-separated list of per-character horizontal offsets, allowing selective displacement of individual characters or words within a single text run. textLength compresses or expands the entire text run to a specified pixel width. lengthAdjust controls whether compression affects glyph spacing or glyph shapes. writing-mode rotates the entire text orientation. These attacks operate at the text-element level and are distinct from tspan-level attacks — they require separate detection logic and are frequently missed by auditors focused on child element inspection.
Finding SA-TXTA-001: dx list values displace consent text characters off-screen after index N
<text> element carries a dx attribute with a space-separated list of per-character horizontal offsets. The list values are zero for the first N characters (which appear normally), then contain a large value (e.g., 5000) at position N+1. Per SVG text rendering, a dx list applies the offset at the indexed character and the offset persists cumulatively for all subsequent characters — once the 5000-unit offset fires at character N+1, all remaining characters render 5000 units to the right of the SVG viewport. The consent text appears truncated at a natural-looking word boundary; the remaining characters (including key scope descriptions like "including file access") are off-screen.<!-- dx list: 0 0 0 ... 0 5000 0 0 ... --> <!-- Characters 0-17 render normally: "I authorize all r" --> <!-- Character 18 receives dx=5000: jumps 5000px right --> <!-- Characters 19+ inherit the 5000-unit accumulated offset --> <text x="10" y="40" font-size="14" fill="#111827" dx="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"> I authorize all requested MCP server permissions including file and network access </text>
SVG dx list values are per-character, not per-word. The list index aligns with the Unicode code point sequence of the text content, including spaces. Choosing the split index to coincide with a word boundary (e.g., after "I authorize all r") makes the visible portion appear as a complete phrase. The jump value (5000 SVG user units) typically places the remaining text well beyond the SVG viewport. The textContent of the element is the full consent string; the dx attribute must be parsed as a list and the cumulative offset algorithm applied to detect the displacement point.
Finding SA-TXTA-002: textLength="1" on the text element compresses the full sentence to 1px wide
textLength="1" with the default lengthAdjust="spacing". SVG textLength forces the browser to render the entire text run within the specified pixel width. With textLength="1" and a sentence of 80 characters, each character is allocated approximately 0.0125px of horizontal space — the entire consent sentence renders as a 1-pixel-wide line, invisible on any display. The element has a valid position, the fill color is set correctly, and textContent returns the full consent string. Only reading the textLength attribute and comparing it against the expected rendered width of the text reveals the attack.<!-- textLength="1" forces entire text run into 1px width -->
<!-- lengthAdjust="spacing" adjusts letter-spacing (default) -->
<!-- but spacing cannot compress 80 chars into 1px realistically -->
<!-- browsers clamp behavior; in practice text renders at sub-pixel scale -->
<text x="10" y="40" font-size="14" fill="#111827"
textLength="1" lengthAdjust="spacing">
I authorize all requested MCP server permissions including file and network access
</text>
<!-- Compare: button uses a separate text element with normal textLength -->
<text x="300" y="80" font-size="14" fill="white" text-anchor="middle"
textLength="80" lengthAdjust="spacing">Agree</text>
The attack calibration targets the contrast between consent text and button label. The button label "Agree" (5 characters, textLength="80") renders at 16px per character — visually normal. The consent text (80 characters, textLength="1") renders at 0.0125px per character — invisible. An auditor checking that font-size is the same on both elements will find 14px on both. Detection requires reading textLength and computing the effective per-character width: effectiveCharWidth = textLength / textContent.length.
Finding SA-TXTA-003: lengthAdjust="spacingAndGlyphs" with small textLength deforms glyph shapes
textLength="20" with lengthAdjust="spacingAndGlyphs". The spacingAndGlyphs value distributes the compression across both letter-spacing and glyph shapes — each glyph is horizontally scaled (compressed or stretched) to fill the target width. With a 60-character consent sentence compressed to 20px total width, each glyph is scaled to approximately 0.33px wide. Unlike lengthAdjust="spacing" which only adjusts spaces between glyphs, spacingAndGlyphs deforms the character shapes themselves, making individual letters unrecognizable even if the rendering is visible at all. The consent text appears as a thin bar of compressed symbols.<!-- spacingAndGlyphs: both spacing AND glyph shapes are compressed -->
<!-- 60-char sentence into 20px = each glyph is 0.33px wide -->
<text x="10" y="40" font-size="14" fill="#111827"
textLength="20" lengthAdjust="spacingAndGlyphs">
I authorize all MCP server permissions
</text>
<!-- For comparison: button "Agree" at normal textLength -->
<rect x="260" y="55" width="120" height="30" fill="#4f46e5" rx="6"/>
<text x="320" y="75" font-size="14" fill="white" text-anchor="middle">
Agree
</text>
Finding SA-TXTA-004: writing-mode="tb" rotates consent text to vertical orientation
writing-mode="tb" (top-to-bottom). This rotates the text rendering so that characters flow vertically rather than horizontally. The consent sentence is rendered as a vertical column of characters, reading from top to bottom. While the text is technically visible if the user knows to look for vertical text, users habituated to horizontal consent dialogs will not naturally read a vertical text column as the consent disclosure. The element position, fill, and font-size are all within normal ranges — only the writing direction is anomalous.<!-- writing-mode="tb": text flows top-to-bottom (vertical) -->
<!-- The consent text is rendered as a thin vertical strip -->
<!-- Users reading left-to-right may not notice it at all -->
<text x="380" y="10" font-size="14" fill="#111827"
writing-mode="tb">
I authorize all MCP server permissions
</text>
<!-- Button remains horizontal and prominent -->
<rect x="100" y="40" width="200" height="40" fill="#4f46e5" rx="8"/>
<text x="200" y="66" font-size="16" fill="white" text-anchor="middle">
I Agree to Everything
</text>
writing-mode="tb" is a valid SVG presentation attribute inherited from CSS writing modes. It is commonly used for CJK (Chinese, Japanese, Korean) vertical text. In a consent dialog context, it is anomalous. The consent text starts at x="380" (near the right edge) and flows downward — in a typical 400-unit SVG, this places the vertical text in a narrow strip at the right margin, easily overlooked. An audit checking font-size, fill, and opacity will not flag this; detection requires checking writing-mode and glyph-orientation-vertical on consent text elements.
text element attribute attacks vs tspan child element attacks
| Attribute | Scope | Attack pattern | Detection requirement |
|---|---|---|---|
| dx (list on text element) | Per-character across the full text run | Large value at index N displaces characters N onwards off-screen | Parse as space-separated list; apply cumulative offset algorithm; check all character positions |
| textLength on text element | Entire text element's run width | textLength="1" compresses 80-char sentence to 1px total width | Compute effectiveCharWidth = textLength / characterCount; flag below legibility threshold |
| lengthAdjust on text element | Controls how textLength compression is distributed | spacingAndGlyphs deforms glyph shapes; text unreadable at extreme compression | Evaluate textLength + lengthAdjust together; spacingAndGlyphs requires lower textLength threshold to flag |
| writing-mode on text element | Changes text flow direction for the element | writing-mode="tb" rotates consent text to vertical — not naturally read by LTR users | Flag non-horizontal writing-mode on consent text elements as Medium severity |
| dx on tspan child | Per-span offset applied to the tspan's characters | tspan dx moves a span's content relative to prior tspan | Distinct from text-element dx; requires tspan traversal |
SkillAudit audits SVG text element attributes separately from tspan child attributes. It parses dx lists as character-indexed arrays and applies the cumulative offset algorithm to find displacement points; checks textLength against minimum legible widths; evaluates lengthAdjust mode to determine the appropriate compression threshold; and flags non-standard writing-mode values on consent text. Run a free audit on your MCP server GitHub URL.