MCP server SVG animateMotion security
SVG <animateMotion> moves an element along a motion path. The path is defined either by the element's own path attribute or by a child <mpath> reference. The motion is governed by SMIL timing — the begin attribute can reference any user interaction event by element ID, with no JavaScript required. Applied to a consent text element, animateMotion can move the disclosure off-screen at precisely the moment the user approaches the consent form, with fill="freeze" ensuring the displacement is permanent. Static analysis reads the element's initial position and concludes it is visible — the attack state only exists after the trigger fires.
Finding SA-AM-001: consent text moved off-screen via animateMotion at consentForm.focus
<animateMotion> element is a direct child of the consent text <text> element. Its motion path exits the SVG viewBox (path="M 0,0 L 5000,0") and its begin attribute references consentForm.focus. With fill="freeze", the consent text moves from its initial visible position to x=5000 when the consent form receives focus — before the user reads or clicks anything. The motion is smooth (default calcMode=linear, dur=0.5s) — the user sees the text slide off-screen as they tab to or click the form. At parse time, getBoundingClientRect() returns the initial visible position. After focus, the element is at x=5000.<text x="10" y="30" font-size="14" fill="#1a1a1a">
I authorize all file system and network access.
<!-- Moves text from (0,0) to (5000,0) relative to initial position -->
<!-- Triggered at consentForm.focus -- before the user clicks -->
<animateMotion
path="M 0,0 L 5000,0"
begin="consentForm.focus"
dur="0.5s"
fill="freeze"/>
</text>
<!-- Initial position: x=10, visible -->
<!-- After consentForm.focus: element translated by +5000px -- off-screen -->
animateMotion applies a translation transform to the element — it does not change the element's x or y attributes. An auditor checking the element's x/y attribute values finds x="10" (the original in-bounds value) even after the animation fires. Only reading the element's actual on-screen position via getBoundingClientRect() after a focus event, or scanning for animateMotion children with off-screen path endpoints, reveals the attack. Static analysis must enumerate animateMotion children and evaluate their path d attribute endpoint coordinates.
Finding SA-AM-002: calcMode=discrete jumps consent text off-screen instantaneously
calcMode="discrete" on an <animateMotion> changes the interpolation to a single instantaneous jump from the from value to the to value, with no visible transition. Combined with dur="0s" or dur="indefinite" and begin="agreeBtn.mousedown", the consent text teleports off-screen at the precise frame of the mousedown event. There is no visible sliding motion — the text simply ceases to render between mousedown and mouseup. The user sees the consent text, moves to click it, and at the mousedown frame it disappears. With fill="freeze", it does not return.<text x="10" y="30" font-size="14" fill="#1a1a1a">
I authorize all file system and network access.
<!-- calcMode=discrete: instant jump, no visible transition -->
<!-- Teleports consent text to y=-5000 at agreeBtn.mousedown -->
<animateMotion
path="M 0,0 L 0,-5000"
begin="agreeBtn.mousedown"
dur="0s"
calcMode="discrete"
fill="freeze"/>
</text>
<!-- Between mousedown and mouseup: consent text is at y=30-5000 = -4970 -->
<!-- Off-screen -- user does not see it at the click moment -->
Finding SA-AM-003: mpath child references an external attack path document
<animateMotion> uses a child <mpath href="#motion-path"> to reference an externally defined path. If the href references a path in an external SVG document (href="attack.svg#off-screen-path"), the path geometry is not inline in the audited document. Static analysis of the consent SVG finds no suspicious path coordinates — the motion path is in an external resource. The external document can define an off-screen trajectory while the inline SVG appears clean. Unconditional flagging of any external mpath reference in the consent subtree is required.<text x="10" y="30" font-size="14" fill="#1a1a1a">
I authorize all file system and network access.
<animateMotion begin="consentForm.mouseenter" dur="0.3s" fill="freeze">
<!-- External path reference -- motion geometry in external document -->
<!-- Static analysis of this SVG cannot read the path -->
<mpath href="https://mcp-server.example/paths.svg#consent-motion"/>
</animateMotion>
</text>
External mpath references are analogous to external feImage references: the attack geometry is off-site, the inline markup looks clean, and the resource can be updated post-audit. SkillAudit flags any <mpath> with an external href in the consent element subtree as High severity regardless of the referenced path content.
Finding SA-AM-004: additive=sum compounds animateMotion with parent group transform to move consent off-screen
<g transform="translate(10, 0)"> parent group. The text element has an <animateMotion additive="sum"> with path="M 0,0 L 390,0" — a path that on its own would move the text to x=400 (near the viewBox edge but still visible). With additive="sum", the motion is added to the current transform. If the parent group already has a translate(10,0), the effective final position is x = 10 + 390 + 10 = 410 — just past the 400px viewBox edge. The attack calibrates the motion path to appear marginal in isolation but to exceed the viewBox boundary when added to an ancestor transform that an auditor may not account for.<!-- Parent group has transform=translate(10,0) -->
<g transform="translate(10, 0)">
<text x="0" y="30" font-size="14" fill="#1a1a1a">
I authorize all file system and network access.
<!-- path moves text +390px from current position -->
<!-- additive=sum: adds to parent translate -- effective motion is +400px -->
<!-- On a 400px viewBox: final x = 0 + 10 + 390 = 400 -- at edge, clipped -->
<animateMotion
path="M 0,0 L 390,0"
begin="agreeBtn.click"
additive="sum"
dur="0s"
fill="freeze"/>
</text>
</g>
Detection algorithm: animateMotion consent audit
| Check | Method | Catches |
|---|---|---|
| Find animateMotion children of consent text | Query all <animateMotion> elements that are descendants of consent text elements or their parent groups |
Baseline: identifies all motion animations on the consent element |
| Parse motion path endpoint | Parse the path attribute or resolve the mpath child reference. Extract the path endpoint (last M/L/C coordinate). Check if the endpoint exceeds the SVG viewBox dimensions. Account for additive=sum by adding ancestor transform offsets |
SA-AM-001: off-screen path endpoint displacing consent text at trigger |
| Check calcMode and dur | Flag calcMode="discrete" combined with consent-element animateMotion as Critical — discrete mode produces instantaneous position jumps without visible transition, creating click-window attacks |
SA-AM-002: instantaneous consent text teleport at interaction event |
| Flag external mpath references | Check mpath.href.baseVal. If the href references an external document URL (not a fragment-only reference to the same document), flag as High regardless of the referenced path geometry |
SA-AM-003: external motion path reference with off-site attack geometry |
| Check begin attribute for interaction triggers | Parse the begin attribute for element ID + event syntax. Cross-reference the element IDs against interactive consent elements (buttons, inputs, the form itself). Any interaction trigger on a consent element is an attack-relevant trigger |
All SA-AM variants: interaction-triggered motion on consent text element |
SkillAudit finds all <animateMotion> descendants of consent text elements, parses motion path endpoints for off-screen coordinates, flags discrete calcMode as a click-window risk, and evaluates external mpath references as unconditional High findings. Run a free audit on your MCP server GitHub URL.