XSS filter evasion covers many hundreds of methods that attackers can use to bypass cross-site scripting (XSS) filters. A successful attack requires both an XSS vulnerability and a way to inject malicious JavaScript into web page code executed by the client to exploit that vulnerability. The idea of XSS filtering is to prevent attacks by finding and blocking (or stripping away) any code that looks like an XSS attempt. The problem is there are countless ways of bypassing such filters, so filtering alone can never fully prevent XSS. Before going into just a few of the thousands of known filter evasion methods, let‘s start with a quick look at the concept and history of XSS filtering.
What is XSS filtering and why is it so hard to do?
At the application level, XSS filtering means user input validation performed specifically to detect and prevent script injection attempts. Filtering can be done locally in the browser, during server-side processing, or by a web application firewall (WAF). For many years, server-side filtering was mostly used, but eventually browser vendors started building in their own filters called XSS auditors to prevent at least some cross-site scripting attempts from reaching the user.
The idea was that the filter scans code arriving at the browser and looks for typical signs of XSS payloads, such as suspicious tags in unexpected places. Common approaches to filtering included complex regular expressions (regex) and code string blacklists. If potentially dangerous code was found, the auditor could block either the entire page or just the suspicious code fragment. Both reactions had their disadvantages and could even open up new vulnerabilities and attack vectors, which is why integrated browser filters soon went away.
All approaches to filtering have their limitations. XSS filtering by the browser is only effective against reflected XSS attacks, where the malicious code injected by the attacker is directly reflected in the client browser. Client-side filters and auditors are no use against XSS where the attack code is not parsed by the browser, including DOM-based XSS and stored XSS. Server-side and WAF-based filters can help against reflected and stored XSS but are helpless against DOM-based attacks since these happen entirely in the browser and the exploit code never arrives at the server. On top of that, trying to do XSS filtering in the web application itself is extremely complicated, can have unintended consequences, and requires constant maintenance to keep up with new exploits.
How attackers bypass cross-site scripting filters
At best, XSS filtering adds an extra level of difficulty to the work of attackers crafting XSS attacks, as any injected script code first has to get past the filters. While XSS attacks generally target application vulnerabilities and misconfigurations, XSS evasion techniques exploit gaps in the filtering performed by the browser, server, or WAF.
There are numerous evasion approaches that can be combined to build countless bypasses. The common denominator is that they abuse product-specific implementations of web technology specifications. A large part of any browser’s codebase is devoted to gracefully handling malformed HTML, CSS, and JavaScript to try and fix code before presenting it to the user. XSS filter evasion techniques take advantage of this complex tangle of languages, specifications, exceptions, and browser-specific quirks to slip malicious code past the filters.
Examples of XSS filter bypasses
Filter evasion attempts can target any aspect of web code parsing and processing, so there are no rigid categories here and the list is always open. The most obvious script
tag injections will generally be rejected out of hand, but there are many more sophisticated methods, and you can also use other HTML tags as injection vectors. Event handlers, in particular, are often used to trigger script loading, as they can be tied into legitimate user actions and are hard to just remove without breaking functionality. Commonly exploited handlers include onerror
, onclick
, and onfocus
, but the majority of supported event handlers can be used as XSS vectors.
To give you some idea of the huge number of ways to bypass an XSS filter, the long list below is still only a tiny fraction of the tools available to attackers (see the OWASP Cheat Sheet for a scarily detailed list based on RSnake’s original cheat sheet). While this post is definitely not a complete reference, and most examples will only work in specific scenarios, anyone familiar with JavaScript should be aware that many such quirks exist alongside what you’d normally consider valid syntax.
Character encoding tricks
To bypass filters that rely on scanning text for specific suspicious strings, attackers have a variety of ways to encode one or many characters. Encodings can also be nested, so you’re encoding the same string many times, potentially using different methods. The choice of encoding is also dependent on the context, as browsers encode and decode characters differently in different places (for example, URL encoding is only supported for URL values in href
tags). The following examples show just a few possibilities, and that’s without even resorting to Unicode tricks.
To bypass filters that directly search for a string like javascript:
, some or all characters can be written as HTML entities using ASCII codes:
Click this link!
To evade filters that look for HTML entity codes using a pattern of followed by a number, you can use ASCII codes but in hexadecimal encoding:
Click this link!
Base64 encoding can be used to obfuscate attack code. This example also displays an alert saying “Successful XSS”:
All encoded character entities can be from 1 to 7 numeric characters, with any initial padding zeroes being ignored. This gives each entity in each encoding several extra zero-padded versions (OWASP’s XSS filter evasion cheat sheet lists no less than 70 valid ways of encoding just the <
character). Also, note that semicolons are not actually required at the end of entities:
Click this link!
Character codes can be used to hide XSS payloads: