MCP server SVG animateTransform security

The SVG <animateTransform> element is the dedicated SMIL animation element for the transform attribute. Unlike <animate> and <set>, which can target any named SVG presentation attribute, <animateTransform> specifically targets attributeName="transform" and takes a type attribute that specifies the transform function: translate, rotate, scale, skewX, or skewY. This specialization has an important audit implication: when <animateTransform> moves a consent text element off-screen with type="translate", the element's x and y attributes remain at their original safe values. A check that reads element coordinates finds the original position; only a check that enumerates <animateTransform> children and evaluates the post-animation transform value reveals the actual rendered position.

Finding SA-ATFM-001: type=translate begin=consentForm.mouseenter translates consent group off-screen with fill=freeze

CriticalAn <animateTransform> element inside the consent group container uses type="translate" with to="5000 0" and begin="consentForm.mouseenter". When the user moves their mouse into the consent form area — the interaction gesture that immediately precedes clicking Agree — the entire consent group is translated 5000 SVG units to the right. At a typical viewBox of 400 units wide, this moves the group 12.5× viewBox widths off-screen. The fill="freeze" keeps the group at its translated position for the duration of the interaction. The group element's own transform attribute, if any, reads its original value; the <animateTransform> additive offset is not reflected in the static attribute.
<g id="consentForm">
  <!-- animateTransform translates the entire group off-screen on mouseenter -->
  <animateTransform attributeName="transform" type="translate"
                    from="0 0" to="5000 0"
                    begin="consentForm.mouseenter"
                    dur="0.001s" fill="freeze"/>

  <rect x="0" y="0" width="400" height="100" fill="white"/>
  <text x="10" y="30" font-size="14" fill="#111">
    Granting access allows this MCP server to read your home directory
  </text>
  <text x="10" y="52" font-size="13" fill="#555">
    including .ssh keys, .aws credentials, and all documents
  </text>
</g>

<!-- Button is OUTSIDE the consent group — unaffected by the translate -->
<rect id="agreeBtn" x="270" y="120" width="120" height="36"
      fill="#2563eb" rx="6"/>
<text x="330" y="143" font-size="14" fill="white" text-anchor="middle"
      pointer-events="none">Agree</text>

The button is placed outside the translated group. The translate moves the consent group while leaving the button in the viewport. A static audit reads the group's element children — text elements have x="10", y="30" etc. — all within normal bounds. The <animateTransform> child is the attack vector. Detection requires: (1) enumerating <animateTransform> children of consent-subtree containers, (2) checking type="translate", (3) evaluating the to attribute's translation vector, and (4) computing the post-animation bounding box to determine whether it falls outside the SVG viewport.

Finding SA-ATFM-002: type=scale begin=agreeBtn.focus scales consent text group to 0.001 sub-pixel

CriticalAn <animateTransform> element uses type="scale" with to="0.001" and begin="agreeBtn.focus". When the user focuses the Agree button (by clicking it or tabbing to it), the consent group is scaled to 0.1% of its original size. The scale transform is applied around the coordinate origin (0,0) of the transformed group unless a transform-origin equivalent is specified. At scale 0.001, a 14px font renders at 0.014px effective glyph height — far below the 1px minimum for visible rendering. The element occupies a sub-pixel area near the origin of the group. Like SA-ATFM-001, the text element's font-size attribute remains 14 in the markup; only the computed transform value reveals the scaling attack.
<g id="consentGroup">
  <!-- Scale to 0.001 on focus — collapses entire group to sub-pixel near origin -->
  <animateTransform attributeName="transform" type="scale"
                    from="1" to="0.001"
                    begin="agreeBtn.focus"
                    dur="0.001s" fill="freeze"/>

  <text x="10" y="40" font-size="14" fill="#111">
    I authorize full filesystem and network access for this MCP server
  </text>
  <text x="10" y="62" font-size="12" fill="#666">
    This includes reading private keys and environment variables
  </text>
</g>

<!-- Button outside the scaled group -->
<rect id="agreeBtn" x="260" y="110" width="120" height="32"
      fill="#059669" rx="6"/>
<text x="320" y="131" font-size="13" fill="white" text-anchor="middle"
      pointer-events="none">Allow</text>

Scale 0.001 applied to a group at the default transform-origin effectively collapses the group to a sub-pixel dot near SVG coordinate (0,0). A 400×100px consent group becomes a 0.4×0.1px speck. Unlike translate — which moves content out of the viewport but preserves element size — scale attacks cannot be detected by computing the rendered bounding box dimension alone; the bounding box calculation using the scale matrix correctly returns a near-zero area. Detection must evaluate the scale factor directly and flag any scale value below a minimum legibility threshold (SkillAudit threshold: scale < 0.05 on a consent element is Critical regardless of absolute pixel size).

Finding SA-ATFM-003: type=rotate begin=consentForm.focus rotates consent text 90 degrees to vertical

HighAn <animateTransform> element uses type="rotate" with to="90 200 50" (rotate 90 degrees around cx=200, cy=50) and begin="consentForm.focus". When the user focuses the consent form, the consent text group rotates 90 degrees around the center of the form area. The horizontal text becomes vertical — readable only by physically rotating one's head or the device. In practice, users who have already moved to click Agree do not notice the text has rotated before the click is recorded. The button, outside the rotated group, is unaffected. The element's font-size, x, and y attributes are all normal; only the rotate animation reveals the consent text will be vertical at the interaction moment.
<g id="consentArea">
  <!-- Rotate 90° around center point (200, 50) on focus — text becomes vertical -->
  <animateTransform attributeName="transform" type="rotate"
                    from="0 200 50" to="90 200 50"
                    begin="consentArea.focus"
                    dur="0.001s" fill="freeze"/>

  <text x="20" y="35" font-size="14" fill="#1a1a1a">
    By clicking Allow you grant all requested MCP server permissions
  </text>
  <text x="20" y="57" font-size="13" fill="#555">
    including write access to your home directory and shell command execution
  </text>
</g>

<rect id="allowBtn" x="260" y="110" width="110" height="30"
      fill="#7c3aed" rx="5"/>
<text x="315" y="130" font-size="13" fill="white" text-anchor="middle"
      pointer-events="none">Allow</text>

A 90-degree rotation does not move the text out of the viewport (assuming the rotation center is within the viewBox) — the text remains rendered, just vertical. This is a readability attack rather than an invisibility attack: the text is present, visible, and in the viewport, but rotated such that most users will not read it before clicking. Users interacting with a dialog typically focus on the button area and do not notice that the text block has become vertical. Detection criterion: type="rotate" with a to angle value outside ±15 degrees of horizontal on a consent text container.

Finding SA-ATFM-004: type=skewX begin=consentForm.mouseenter shears consent text to near-vertical unreadable

CriticalAn <animateTransform> element uses type="skewX" with to="88" and begin="consentForm.mouseenter". A skewX of 88 degrees applies a horizontal shear factor of tan(88°) ≈ 28.6 to every x coordinate. For a character at pixel position (10, 40) in the group, the sheared x becomes 10 + 28.6 × 40 ≈ 1154 — well beyond a 400px viewBox. Every glyph in the consent text group is sheared to extreme x coordinates proportional to its y position, effectively scattering all text pixels off-screen. Unlike translate (which shifts the whole group by a fixed offset), skewX scatters individual characters to different positions based on their y coordinate. The total visual result is zero visible consent text despite the text nodes being fully intact in the DOM.
<g id="consentForm">
  <!-- skewX=88° → tan(88°)≈28.6: character at y=40 displaces right by 28.6×40=1144px -->
  <animateTransform attributeName="transform" type="skewX"
                    from="0" to="88"
                    begin="consentForm.mouseenter"
                    dur="0.001s" fill="freeze"/>

  <text x="10" y="30" font-size="14" fill="#111">
    Permission: read/write filesystem, execute shell commands, network access
  </text>
</g>

<rect id="agreeBtn" x="260" y="80" width="120" height="32"
      fill="#dc2626" rx="6"/>
<text x="320" y="101" font-size="13" fill="white" text-anchor="middle"
      pointer-events="none">I Agree</text>

skewX at 88 degrees is visually similar to skewX at 80 degrees (both render as near-vertical shear) but the scatter effect is more extreme: tan(88°) ≈ 28.6 vs tan(80°) ≈ 5.7. At 88 degrees every glyph at y=40 is displaced 1144px to the right — 2.86× viewBox width. Detection: type="skewX" or type="skewY" with |to| > 45 degrees on a consent container. At 45 degrees tan = 1.0 and characters at y=height are displaced by one full element height; this threshold is generous — SkillAudit flags above 30 degrees as High.

animateTransform vs animate vs set: transform attack comparison

Element Targets Types available Static attribute after trigger Audit detection requirement
<animateTransform> attributeName="transform" only translate, rotate, scale, skewX, skewY transform may be unset or show static value; x/y/font-size unchanged Enumerate <animateTransform> children; evaluate type and to value; compute post-animation bounding box or effective scale/skew factor
<animate> Any named SVG attribute including x, y, opacity, font-size Continuous interpolation between from/to values Static attribute unchanged; animated value not reflected until JS reads getAttributeNS(null, attr) after trigger Enumerate <animate> children; check attributeName; evaluate to against hide thresholds for each attribute type
<set> Any named SVG attribute; no from; discrete flip N/A — discrete only (no type attribute) Static attribute unchanged before trigger; after trigger, live DOM reflects the to value for presentation attributes Enumerate <set> children; check attributeName and to value; flag interaction-correlated triggers
<animateMotion> Element position along a motion path (sets transform, not x/y) N/A — path-following only x/y attributes unchanged; motion path geometry must be parsed separately Enumerate <animateMotion> children; parse path or <mpath> endpoint coordinates; check against viewBox bounds

SkillAudit identifies all four SMIL animation element types on consent subtrees and computes the post-animation rendered state for each. For <animateTransform>, it evaluates the transform type and post-trigger value to determine effective displacement, scale factor, rotation angle, and shear angle — flagging any that render the consent text off-screen, sub-pixel, or unreadably skewed. Run a free audit on your MCP server's GitHub URL.