Building XSS Polyglots

XSS polyglots are quite popular among beginners and lazy XSS testers since they only require a single copy and paste. Although doomed to be easily flagged by any decent filter or WAF, they can be useful to spot most of the XSS cases out there.

Here we will try to build a cost-effective XSS polyglot, with the least amount of characters possible and the maximum and best XSS cases we can come with. We will also add some key elements for validation and even some basic evasion tricks.

To test and validate our polyglot we will use our new default test page XSS GYM. It is designed to provide a common environment exactly for this type of XSS construct, in order to make its understanding easier.

A polyglot payload is some payload that triggers for different vulnerabilities or contexts. You can find an example of the former here. Some polyglots for XSS were made in the past but here we will bring a new one based in the same logic of those but adding new tricks discovered for XSS (some only published here in this very blog).

*** ATTENTION: this blog uses different formatting and this affect some characters, specially quotes and dashes. Don’t just copy and paste payloads from here without replacing them or USE THE PAYLOAD IN TWEET at the end of this post. *** 

We start by choosing the core of our work: the payload that triggers in the simplest HTML injection context:


We use an arbitrary XHTML tag (K from “KNOXSS”) that pops the alert box automatically with the help of “contenteditable” and “autofocus” HTML attributes to support the event handler “onfocus”. That arbitrary tag in combination with this handler work in Chromium-based browsers while for Mozilla Firefox there’s a need to use another HTML element that supports those attributes like the “input” tag (at the time of this publishing).

Another choice here is the mixed case and enclosing the “alert” keyword  with a pair of parentheses, both aimed at basic filter evasion (cost-effective).

We proceed by adding the next set of tricks to cover some needed injection scenarios: those which require the breakout of current pair of HTML tags, both opening (<tag>) and closing (</tag>).


There are more HTML tags that require that kind of breakout but those are the main ones and our choice here. Notice there’s no need to use a > for every tag, just one at the end since our payload will land in just one of those and all the others will be ignored.

Now let’s build the set responsible for Javascript injections, which require breaking out from a string. It will be useful too for our inline HTML injections because our first construct above comes with “contentEditable/autoFocus/OnFocus=(alert)(1)” snippet.

We will just fix that little syntax error for inline HTMLi with a // (JS comments) needed for escaped JS injections along with a pair of brackets ( { } ) to allow correct code evaluation (like the minus sign in ‘-alert(1)). Finally another pair of comments (/**/) to end comments opened in the initial set of string delimiter breaking (/*).

Javascript parser will ignore any other opening of comments (/*) while a closing one (*/) is not found.


In that way, the first quote (or backtick) that breaks out, with or without a backslash to “escape the escape”, will start a multi line JS comment section with /* that will end in the first */, right before the alert code. It’s then commented out with // because we can’t really guess how to fix the syntax after that (also there’s no other way for escaped cases).

Check how it works detailed with examples below highlighting actual evaluated code. The first one is a simple JS injection with single quotes breaking and the second one is an escaped double quotes injection (cases #6 and #7 here).



Now let’s add some more nice tricks to our payload, before forking it: a fork is needed because some tricks get in conflict so we need to use one or another. The first one is a bypass against a filter for comments only:


We’ve added a <!–> in the beginning and a double dash right before the last greater than sign (>). That bypass filters like “/<!–.*–>/” and alike.

Next trick is a CSP bypass. It’s also useful for any blacklist-based filter that do not blacklist <base> tag, including the implemented CSP itself:


Since base element cares only about the domain, as long as we provide one (in this case domain is set to reply to any relative path based script call) it will work regardless of URL path, allowing anything after slash.

To be able to bypass sanitizing with HTML entities, let’s add another trick for DOM-based scenarios which might be effective too against WAFs.


If there’s sanitizing of < as &lt; but the above payload gets inserted into DOM with some dangerous JS function (a sink), the “\74k<K/” will become “<k&lt;K/” (\74 is < in octal) which is a valid arbitrary tag to be used with OnFocus and its attributes above. Closing the DOM insertion with \76 which is “>” in octal, replacing base href slash because a backslash is turned into a slash in the actual browser request to an HTTP Reference (href).

We can check this working here.

Let’s add one more curious case to our “breaking delimiters” set which is a HTML entity based bypass for quote filtering inside an event handler, when our injection lands inside a function for example. A live test page for it is provided here.


It works because HTML entities are supposed to work in HTML space at a higher level than javascript code. Closing the function with “)” is mandatory as well as the “;” right before our {(alert)(1)} code.

Next comes a complicated one: in order to make template literals placeholder injection work, we use an intricate nesting of JS comments to handle all previous cases plus this new one.


Initial /* is for the OnFocus event and the /*/ is for the placeholder injection while it’s also for the breaking delimiters set. That /*/ is a double edged piece, working both as comments opening and closing, depending on the previous code invoked. Below, the 3 possible cases highlighted:




Now the core of our polyglot is almost over because all new tricks we add will invalidate some other one.

By adding a couple of CRLF (Carriage Return and Line Feed) we add some HTTP header injection capabilities, both by changing content type and by trying to force our response body to deliver our code.


Unfortunately this very one might disable our DOM-based XSS trick with octals.

So in order to keep it working let’s forget about it for our next useful addition:


The Javascript pseudo-protocol is used for Location Sink DOM-based XSS with tricks to bypass URL validation filters but instead of ternary operator we use optional chaining to come with the needed question mark. It also works against PHP strip_tags function in anchors. Our previous first slash is used with the new alert?.(1) slash to form the double one needed for commenting the rest of payload.

Notice that we have replaced <!–> which makes it less useful since it works better in the beginning for a filter that allows only HTML comments for the whole input. A minor drawback though, highly compensated by the new addition.

One last trick involves the use of a backslash in the end of our paylaod for quoteless Javacript injections scenarios:


But keep in mind this backslash in the end might break native JS code that doesn’t get injected by any of our previous payloads and doesn’t escape or sanitize that character. It might not pay off so we can skip it.

Finally here’s our polyglot of choice:

It’s able to pop the alert box in at least 27 (one case requires click) of 30+ XSS test cases we currently have in our XSS Gym.

Try it out!


P.S.: The question mark of “JavaScript://%250Aalert?.(1)” makes it impossible to catch the URL case (PHP_SELF) featuring here due to the mimic of an URL query string. Like stated above, there’s a trade off for every new trick added to the core payload.