MCP server SVG g element transform security
The SVG <g> element groups child SVG elements and applies a single transform attribute to all of them as a unit. The transform propagates down the child tree: a scale(0.001) on a group containing consent text reduces every child element's rendered size by a factor of 1000, regardless of the child's own font-size or x/y attributes. The consent text children look completely normal when inspected individually — correct font-size, correct fill, correct position within the group's coordinate system. The attack lives on the parent <g>. Auditors that check text element presentation attributes without evaluating ancestor group transforms miss the entire group-transform attack surface.
Finding SA-GTR-001: transform=scale(0.001) on consent text group — all child text rendered at sub-pixel size
<g> element wrapping all consent text carries transform="scale(0.001)". This scales the group's coordinate system by 0.001 in both dimensions. Child text elements with font-size="14" render at an effective font size of 14 × 0.001 = 0.014px — invisible on any display. Child positions (x="10" y="30") map to 0.01px, 0.03px — the entire group renders within a 1-pixel area. The button is outside this group and is unaffected. An audit that checks font-size on each text element finds 14 on all of them; only the group transform reveals the sub-pixel scaling.<svg viewBox="0 0 400 100">
<!-- Group with scale(0.001): all children become sub-pixel -->
<g transform="scale(0.001)">
<!-- font-size=14, but effective size = 14 * 0.001 = 0.014px -->
<text x="10" y="30" font-size="14" fill="#111827">
I authorize all requested MCP server permissions
</text>
<text x="10" y="52" font-size="12" fill="#6b7280">
including read/write file system access
</text>
</g>
<!-- Button is outside the scaled group — renders normally -->
<rect x="240" y="60" width="140" height="32" fill="#4f46e5" rx="6"/>
<text x="310" y="81" font-size="14" fill="white" text-anchor="middle">
Agree
</text>
</svg>
The scale(0.001) transform is calibrated to be audit-invisible at the text element level while being rendering-invisible at the pixel level. The consent text elements are within the <g> coordinate system, where their attributes look reasonable. Only when the group's transform is applied do the coordinates resolve to sub-pixel values in the SVG viewport. Detection requires computing the cumulative CTM (current transformation matrix) from the SVG root to each consent text element by multiplying all intermediate group transforms.
Finding SA-GTR-002: transform=rotate(90) renders consent text vertically, button remains horizontal
<g> element wrapping consent text carries transform="rotate(90)". This rotates the entire group 90 degrees around the SVG origin (0,0). The consent text is rendered as a vertical column — reading bottom-to-top along the left edge of the SVG element. For users habituated to horizontal text in consent dialogs, the vertical consent text is typically not read as a disclosure statement. The button is outside the rotated group and remains horizontal and prominent. The text elements themselves have no rotation attribute — only the parent group has the rotate transform.<svg viewBox="0 0 400 100">
<!-- rotate(90) around origin: consent text renders vertically along SVG left edge -->
<g transform="rotate(90)">
<!-- After rotate(90), x=10,y=30 maps to page coordinates (-30, 10) -->
<!-- Text flows downward on the left margin, partially clipped by SVG bounds -->
<text x="10" y="30" font-size="14" fill="#111827">
I authorize all requested MCP server permissions including file access
</text>
</g>
<!-- Button outside the group: horizontal, visible, prominent -->
<rect x="200" y="35" width="180" height="40" fill="#dc2626" rx="8"/>
<text x="290" y="61" font-size="16" fill="white" text-anchor="middle">
Grant Access
</text>
</svg>
Finding SA-GTR-003: transform=skewX(80) applies extreme horizontal shear — consent text appears as thin vertical lines
<g> element wrapping consent text carries transform="skewX(80)". SkewX applies a horizontal shear transformation where the shear angle is 80 degrees. At 80 degrees (close to 90-degree vertical shear), each glyph is nearly collapsed horizontally — the characters become extremely narrow vertical lines, approximately tan(80°) ≈ 5.67 units wide-to-tall deformation. The consent text at normal font-size renders as a series of near-vertical slashes that are not recognizable as characters. The text is technically "visible" (non-zero rendered area, non-transparent fill) but is completely illegible due to glyph shear distortion.<svg viewBox="0 0 400 100">
<!-- skewX(80): extreme horizontal shear — glyphs become near-vertical lines -->
<!-- tan(80°) ≈ 5.67 — character width is 5.67x the character height -->
<!-- At font-size=14, each character is ~80px wide (sheared) but only ~14px tall -->
<!-- The sentence of 50 characters would require 4000px horizontal space -->
<!-- most of it off-screen, characters themselves unreadable as glyphs -->
<g transform="skewX(80)">
<text x="10" y="35" font-size="14" fill="#111827">
I authorize all MCP server permissions
</text>
</g>
<rect x="250" y="60" width="130" height="30" fill="#059669" rx="6"/>
<text x="315" y="80" font-size="13" fill="white" text-anchor="middle">
Allow
</text>
</svg>
A skewX(80) transform passes naive visibility checks: the element has non-zero dimensions, non-transparent fill, and non-zero font-size. The rendered output is technically a series of colored pixels — they are just not recognizable as consent text glyphs. Detection requires evaluating the skew angle: a threshold of skewX values above approximately 45 degrees (where tan(θ) > 1, meaning glyphs are wider than they are tall) should be flagged as High severity on consent text groups.
Finding SA-GTR-004: transform=translate(-5000,0) moves entire consent group off-screen to the left
<g> element wrapping consent text carries transform="translate(-5000,0)". This moves the group 5000 SVG units to the left of the viewport origin. The SVG viewport is 400 units wide; the consent text group is rendered 5000 units to the left — completely outside the visible area. The text elements inside the group have x="10" which maps to page coordinates of 10 - 5000 = -4990 SVG units. The overflow="hidden" default on SVG elements clips the off-viewport content. The group's DOM position (before the button in the document) and its text content are both present and correct — only the translate displaces it.<svg viewBox="0 0 400 100" overflow="hidden">
<!-- translate(-5000,0): group renders 5000 units to the left of the viewport -->
<!-- x="10" on child text elements maps to -4990 in viewport space -->
<!-- SVG overflow:hidden clips all content outside the 400-unit viewport -->
<g transform="translate(-5000, 0)">
<text x="10" y="35" font-size="14" fill="#111827">
I authorize all requested MCP server permissions including file and network access
</text>
</g>
<!-- Button at normal position within viewport -->
<rect x="240" y="58" width="140" height="32" fill="#4f46e5" rx="6"/>
<text x="310" y="79" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">Agree</text>
</svg>
Finding SA-GTR-005: transform=matrix(0,0,0,0,0,0) — zero matrix collapses group to a single point
<g> element wrapping consent text carries transform="matrix(0,0,0,0,0,0)". The SVG matrix(a,b,c,d,e,f) transform specifies the full affine transformation matrix. With all coefficients set to zero, the transform collapses the entire group to a single point at the coordinate origin — every child element is mapped to (0,0) with zero dimensions. The element is technically still rendered (it maps to a single pixel at the origin) but has no visible content area. All text is collapsed to a zero-by-zero region. This passes zero-opacity checks (opacity is not 0), zero-dimension checks that use getBoundingClientRect() on the group might return a degenerate rect.<svg viewBox="0 0 400 100">
<!-- matrix(0,0,0,0,0,0): all elements map to a single point at origin (0,0) -->
<!-- SVG matrix(a,b,c,d,e,f) → [a c e; b d f; 0 0 1] -->
<!-- All input coordinates transform to (a*x + c*y + e, b*x + d*y + f) = (0, 0) -->
<g transform="matrix(0,0,0,0,0,0)">
<text x="10" y="35" font-size="14" fill="#111827">
I authorize all requested MCP server permissions
</text>
<text x="10" y="57" font-size="12" fill="#6b7280">
including read/write access to your filesystem
</text>
</g>
<rect x="240" y="58" width="140" height="32" fill="#dc2626" rx="6"/>
<text x="310" y="79" font-size="14" fill="white" text-anchor="middle"
pointer-events="none">Allow All</text>
</svg>
The zero matrix transform is the most aggressive group-level attack: no amount of child element inspection will find normally-positioned, normally-sized text, because the parent transform maps everything to zero dimensions. The transform attribute string "matrix(0,0,0,0,0,0)" is the canonical form; equivalent degenerate matrices include scale(0), scale(0,0), and matrix(0,0,0,0,100,100) (which translates to a non-origin point but still collapses to zero dimensions).
Cumulative transform stack: why nested groups multiply attacks
| Transform | Effect on consent text | Passes naive check | Detection method |
|---|---|---|---|
| scale(0.001) | 14px font-size → 0.014px effective — invisible | Yes — text element font-size is 14 | Multiply CTM; effective font size = fontSize × CTM.scaleX |
| rotate(90) | Text flows vertically — not read as consent disclosure | Yes — no rotation on text element itself | Extract rotation from CTM; flag non-zero rotation angles on consent groups |
| skewX(80) | Glyphs sheared to near-vertical lines — illegible | Yes — no skew on text element, opacity/fill normal | Extract skew angle from CTM; flag angles above legibility threshold (~45°) |
| translate(-5000,0) | Group 5000 units off viewport — clipped by overflow:hidden | Yes — text element x=10 appears within normal bounds | Apply CTM to text element position; check if effective page position is within SVG viewport |
| matrix(0,0,0,0,0,0) | All group content collapsed to zero dimensions at origin | Yes — fill, font-size, and position all look correct on text element | Check CTM determinant; determinant = 0 means singular/degenerate matrix |
SkillAudit computes the cumulative current transformation matrix (CTM) from the SVG root to each consent text element by multiplying all intermediate transform matrices on ancestor <g> elements (and other transformable elements). The effective font size, position, and glyph shape are evaluated in page coordinates, not in local group coordinates. Any CTM that produces sub-pixel font sizes, off-viewport positions, singular matrices, or extreme shear angles on consent text is flagged with appropriate severity. Run a free audit on your MCP server GitHub URL.