Cross Site Scripting

HTML Injection

Basic Concept

HTML Injection happens when untrusted input is inserted into a web page’s HTML structure without proper sanitization. If this happens, an attacker may inject HTML elements, JavaScript, or malicious attributes that can run in the victim’s browser.

With zero protections, the simplest-to-understand injection is:

<script>alert()</script>

This starts JavaScript syntax using the <script> tag and calls alert(). However, there are caveats:

  • Server-side insertion: If the server inserts this code directly into the HTML before serving it, the browser trusts it and executes it.

  • Client-side insertion: If some other JavaScript inserts this code later using .innerHTML or similar, inline <script> won’t run. Many tags behave differently once the page is fully loaded.

Because of this, it’s more reliable to use a payload that triggers an event handler immediately, like:

<img src onerror=alert()>

This works because browsers try to load the image immediately, triggering the onerror handler if the source is invalid. Short alternatives:

<!-- Shortest payload -->
<svg onload=alert()>
<!-- Short but universal -->
<style onload=alert()>

Note: <svg> works everywhere except Firefox in some client-side contexts; <style> works almost everywhere.

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 (&#110; = HTML-encoded n, CyberChefarrow-up-right):

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 domainarrow-up-right 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 you can’t achieve XSS due to hard filters, HTML injection can still be powerful in other ways:

  • One idea is to use DOM Clobberingarrow-up-right, 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()arrow-up-right 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 introductionarrow-up-right, an improved version using @importarrow-up-right, and finally this toolarrow-up-right.

  • 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 Injectionarrow-up-right 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 &lt; and &gt; 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 Sheetarrow-up-right 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 Injectionarrow-up-right:

If these < or > characters are blocked or encoded however, we need to be more clever. Similarly to Attribute Injectionarrow-up-right, 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 SyntaxErrors:

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 arrow-up-rightfor 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 (specarrow-up-right), 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 (sourcearrow-up-right, another examplearrow-up-right):

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 JavaScriptarrow-up-right 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 URLSearchParamsarrow-up-right 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()arrow-up-right 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 Injectionarrow-up-right.

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 (sourcearrow-up-right):

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 hashchangearrow-up-right 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()arrow-up-right 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)

  1. .innerHTML

  2. .innerHTML + DOM

  3. write()

  4. 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.

Last updated