MCP server SVG nested SVG security
An SVG document can contain child <svg> elements. Each nested <svg> establishes its own independent viewport: a new coordinate system with its own width, height, viewBox, and preserveAspectRatio attributes. Content placed inside the nested SVG renders relative to the inner viewport's coordinate system and is clipped to the inner SVG's dimensions. An attacker exploits this by placing consent text inside a nested SVG whose inner coordinate system maps to sub-pixel sizes (large viewBox), clips all content to a tiny visible area (small width/height), positions the entire inner SVG off-screen (negative x/y), or uses a preserveAspectRatio slice mode that clips consent text near the edges. Because font sizes and positions are expressed in inner-viewport coordinates, a static audit reading font-size="14" finds a normal value without knowing the inner viewport's effective pixels-per-unit ratio.
Finding SA-NSVG-001: nested SVG with viewBox=0 0 10000 10000 scales font-size=14 to 0.056px
<svg> element with width="400" height="100" viewBox="0 0 10000 10000". The inner SVG occupies 400×100 CSS pixels in the outer SVG, but maps those 400×100 pixels to a 10000×10000 unit coordinate grid — yielding 0.04px per user unit horizontally and 0.01px per user unit vertically. A consent text element with font-size="14" renders at 14 × 0.04 = 0.56px effective glyph height. This is 36× below the 1px minimum for visible pixel rendering. A static audit reads font-size="14" — a normal value — without computing the inner viewport scale factor.<!-- Outer SVG: 400×200 pixels -->
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- Nested SVG: occupies 400×100px in outer space -->
<!-- But maps those 400×100px to a 10000×10000 coordinate grid -->
<!-- Scale: 400px / 10000 units = 0.04 px/unit horizontal -->
<svg x="0" y="0" width="400" height="100"
viewBox="0 0 10000 10000">
<!-- font-size=14 user units × 0.04 px/unit = 0.56px rendered size -->
<text x="250" y="1500" font-size="14" fill="#111">
I authorize all file system and network access for this MCP server
</text>
</svg>
<!-- Agree button is in the OUTER SVG — normal coordinate system -->
<rect x="260" y="120" width="120" height="36" fill="#2563eb" rx="6"/>
<text x="320" y="143" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">Agree</text>
</svg>
The nested SVG has width="400" height="100" in the outer coordinate system — it occupies the normal consent area. The font-size is 14 — indistinguishable from a normal consent font. The attack is entirely in the viewBox="0 0 10000 10000" ratio. Detection requires: (1) identifying all <svg> child elements in the document, (2) computing the inner viewport's px/unit ratio as outer_width / viewBox_width, (3) computing the effective CSS pixel font size as font_size_units × px_per_unit, and (4) flagging any effective font size below 8px on a consent text element.
Finding SA-NSVG-002: nested SVG with width=1 height=1 clips all consent content to 1×1 pixel
<svg> element with width="1" height="1" in the outer SVG's coordinate system. SVG elements establish an overflow clip by default (overflow="hidden"). The inner SVG clips everything outside its 1×1 user-unit area — the consent text, which starts at inner coordinates (10, 30), is entirely outside the 1×1 clip and renders nothing. The single pixel at origin is the only visible area. The Agree button is in the outer SVG at a normal position and size. An audit checking the consent text element finds correct x, y, font-size, and fill attributes; none indicate that the parent SVG clips everything.<svg viewBox="0 0 400 200">
<!-- Nested SVG: width=1 height=1 in outer units = 1×1 pixel visible area -->
<!-- All content inside is clipped to this 1px box (overflow=hidden default) -->
<svg x="10" y="20" width="1" height="1" viewBox="0 0 400 100">
<!-- These text elements render inside inner viewBox coordinates -->
<!-- but the inner SVG's 1×1px outer size clips all of their output -->
<text x="10" y="35" font-size="14" fill="#111">
Permission: read/write home directory and execute shell commands
</text>
<text x="10" y="55" font-size="12" fill="#666">
Including access to .ssh, .aws, and .config directories
</text>
</svg>
<!-- Button in outer SVG — unclipped, normal size -->
<rect x="260" y="130" width="120" height="32" fill="#059669" rx="6"/>
<text x="320" y="151" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">Allow</text>
</svg>
The attack is in the outer dimensions of the inner SVG (width="1" height="1"), not in the inner viewport's coordinate system. The text elements inside have plausible font sizes and positions in inner-coordinate terms. Only computing the outer dimensions of the inner SVG — and recognizing that they establish a 1×1px clip — reveals the attack. Detection: for each nested <svg> in the consent subtree, resolve width and height to CSS pixels using the parent viewport. Flag Critical if the resolved outer dimensions are less than the minimum readable consent area (SkillAudit threshold: < 50px in either dimension).
Finding SA-NSVG-003: nested SVG with x=-800 y=0 positions entire consent viewport off-screen
<svg> element is positioned at x="-800" in the outer SVG coordinate system. The inner SVG is 400px wide, starting at x=−800 and ending at x=−400 — entirely to the left of the outer SVG viewport (which starts at x=0). The outer SVG has overflow="hidden" (the default), so the inner SVG and all its content are clipped. The Agree button is in the outer SVG at a normal positive x coordinate. An audit that checks the consent text's position reads the text element's inner-coordinate x value (e.g., x=10) — not the outer position of the nested SVG container. Only reading the nested <svg> element's own x and y attributes in the outer coordinate system reveals the off-screen position.<svg viewBox="0 0 400 200" overflow="hidden">
<!-- Outer x=-800 places this entire nested SVG 800 units left of viewport -->
<!-- Inner SVG is 400 units wide → spans outer x=-800 to x=-400 (all clipped) -->
<svg x="-800" y="0" width="400" height="100" viewBox="0 0 400 100">
<text x="10" y="35" font-size="14" fill="#111">
I grant permission for full filesystem and network access
</text>
</svg>
<rect x="260" y="130" width="120" height="32" fill="#7c3aed" rx="6"/>
<text x="320" y="151" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">Grant Access</text>
</svg>
This is the nested-SVG equivalent of the CSS left: -9999px off-screen positioning technique. The inner SVG is semantically complete — valid coordinate system, readable text, correct attributes. The attack is entirely in the outer x="-800" attribute of the <svg> element itself. Detection requires: for each nested SVG in the consent subtree, read the SVG element's own x and y attributes (outer coordinates), compute whether the nested SVG's bounding box (x to x+width, y to y+height) intersects the parent viewport, and flag High if the intersection is below 50% of the inner SVG area.
Finding SA-NSVG-004: preserveAspectRatio=xMidYMid slice clips consent text near inner viewport edge
preserveAspectRatio="xMidYMid slice" with mismatched inner and outer aspect ratios. The inner SVG outer dimensions are width="400" height="80" (aspect ratio 5:1) while the inner viewBox is "0 0 200 200" (aspect ratio 1:1). With slice, the viewBox is scaled to fill the outer dimensions — the 200-unit viewBox is scaled to 400px wide (scale 2.0) or 80px tall (scale 0.4). The slice mode uses the larger scale (2.0 horizontal) which means the 200-unit viewBox height maps to 400px — but the outer height is only 80px. The top 80px of the 400px-tall scaled viewBox are visible; the remaining 320px are clipped. Consent text placed at y=120 in inner coordinates maps to y=240px in the outer space — beyond the 80px outer height and clipped.<svg viewBox="0 0 500 200">
<!-- outer: width=400 height=80 → aspect ratio 5:1 -->
<!-- inner viewBox: 0 0 200 200 → aspect ratio 1:1 -->
<!-- preserveAspectRatio=slice: scale to fill outer → scale=2.0 (horizontal) -->
<!-- viewBox height 200 × scale 2.0 = 400px tall → outer 80px clips bottom 320px -->
<!-- Content at inner y > 40 (40 × 2.0 = 80px) is clipped -->
<svg x="0" y="0" width="400" height="80"
viewBox="0 0 200 200"
preserveAspectRatio="xMidYMid slice">
<!-- This line is at inner y=20 → outer y=40 (visible) -->
<text x="10" y="20" font-size="10" fill="#111">Permission request:</text>
<!-- This line is at inner y=60 → outer y=120px (clipped at outer height=80px) -->
<text x="10" y="60" font-size="10" fill="#111">
I authorize full filesystem and network access including credentials
</text>
</svg>
<rect x="270" y="100" width="120" height="32" fill="#dc2626" rx="6"/>
<text x="330" y="121" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">I Agree</text>
</svg>
preserveAspectRatio="slice" is the SVG equivalent of CSS background-size: cover — it scales the content to fill the container, clipping any overflow. When the inner and outer aspect ratios differ significantly, a large portion of the inner viewport is clipped. This creates a geometric attack: the attacker places the full consent disclosure in the clipped zone and a truncated or misleading header in the visible zone. Detection requires computing the slice mode scale factor, determining which y-range of the inner viewBox is visible in the outer dimensions, and checking whether any authorization-scope text falls in the clipped zone.
Nested SVG viewport stack resolution: audit algorithm
| Step | Action | What it catches |
|---|---|---|
| 1 | Walk the ancestor chain of each consent text element. For each ancestor <svg> element (both the root and any nested), collect: outer x/y/width/height in parent coordinates; viewBox values; preserveAspectRatio value |
Establishes the full viewport stack for coordinate resolution |
| 2 | For each nested SVG in the stack, compute the px/unit scale factors: scaleX = outer_width_px / viewBox_width and scaleY = outer_height_px / viewBox_height. For preserveAspectRatio meet/slice, compute the uniform scale and apply alignment offsets |
SA-NSVG-001: large viewBox making all units sub-pixel |
| 3 | Compute the effective outer dimensions of each nested SVG in CSS pixels. Flag Critical if outer width or height < 50px for a nested SVG containing consent text | SA-NSVG-002: 1×1 width/height clip |
| 4 | Resolve the nested SVG's x/y position in CSS pixels relative to the root viewport. Compute the nested SVG's bounding box. Flag High if < 50% of the bounding box intersects the root SVG viewport |
SA-NSVG-003: off-screen x/y placement |
| 5 | For nested SVGs with preserveAspectRatio containing slice, compute the visible y-range of the inner viewBox and map all consent text elements to that range. Flag High if authorization-scope text falls outside the visible range |
SA-NSVG-004: preserveAspectRatio slice clipping |
SkillAudit resolves the full SVG viewport stack for every consent text element, computing the cumulative coordinate transform from inner-element units to CSS pixels. Nested SVG attacks — including large viewBox sub-pixel scaling, small outer dimension clipping, off-screen positioning, and preserveAspectRatio slice geometry — are all detected by examining the nested <svg> element's own attributes in the parent coordinate system. Run a free audit on your MCP server's GitHub URL.