Dead Security Tech: Browser XSS filters
Whenever the subject of Cross-Site Scripting (XSS) remediation strategies arise, somebody invariably mentions browsers having inbuilt XSS filters that mitigates the vulnerability class. Whilst most testers I meet are comfortable with XSS, its exploitation, and its remediation; they lack knowledge of browser XSS filters. The aim of this blog post is to provide an overview of browser XSS filters and to outline why they are a bad remediation strategy.
Additionally, Chrome recently announced the retirement of its own XSS filter, which means that all modern web browsers have now dropped support for this dead security technology.
To play along at home, I’ve created a little web application to complement the blog post:
Types of XSS
There are two types of XSS attacks:
• Reflected XSS - occurs when a web application displays user input on a web page insecurely, permitting the execution of client-side code in a user’s web browser.
• Stored XSS - sometimes referred to as persistent XSS, occurs in the same way; however, the user’s input is persisted through a storage mechanism.
Server-Side Reflected XSS
A classic example of a server-side reflected XSS is the search field: a user inputs a string to search for, the web application then renders that string on the webpage.
Server-Side Stored XSS
An example of a server-side stored XSS would be a user’s display name. The user input for the display name is stored within a database, this data field is then retrieved on the webpage to display the user’s name.
Client-Side (DOM-Based) Reflected XSS
The example for a reflected DOM-based XSS takes the anchor part of a URL and simple reflects its value on the webpage.
Note 1: Firefox and Chrome URL encode user input from the browser by default in recent versions. Due to this, the second vulnerable injection point uses decodeURIComponent() to URL decode the user input; ensuring the XSS payload triggers in these browsers.
Note 2: You have to paste the URL in a new tab to trigger.
The following URL will trigger the XSS:
Client-Side (DOM-Based) Stored XSS
The example for a stored DOM-based XSS takes user input and stores it within the browser’s localStorage. The value is then rendered on the webpage.
Developers and security boffins alike came up with two key measures to prevent this vulnerability class:
- User input validation to ensure malicious characters are not introduced.
- Context-aware output encoding to ensure user input is securely rendered.
However, these two options seem like a lot of hassle and developers can sometimes simply forget to implement them. Can’t we just shift the responsibility to the browser and have them address it?
Browser XSS Filters
Firstly, the goal of browser XSS filters was to mitigate reflected server-side XSS attacks; stored server-side XSS and DOM-based XSS attacks are unaffected. (Reader Mental Exercise: why do you think this the case?)
The first browser implementation was created by Microsoft, 2nd July 2008 they declared the introduction of the XSS Filter in Internet Explorer 8. A year and a half later, Chromium announced the experimental release of XSS Auditor in Chrome 4. A few months later, 7th September 2010, Mozilla proposed the development of their own XSS filter with the aim to replace the NoScript addon.
Each browser had a slightly nuanced approach for the XSS filter they implemented. But at a high-level overview, browser XSS filters worked in the following way:
- Browser intercepts HTTP request.
- If the request referer header indicates the same site then allow request.
- Determine if the request, both GET and POST, contained data fields that match the XSS blacklist.
- Confirm that the payload is reflected in the HTTP response.
- If true, block or filter the payload depending on the XSS filter configuration.
Here are some further reading links that explain the process more in-depth:
Why doesn’t the filter work for other XSS Types?
The reason browser XSS filters does not prevent stored XSS attacks is because, as the data is provided by a trusted source, there is no reliable method for differentiating between malicious and permitted data. For client-side XSS, the browser XSS filters were simply not designed to prevent them.
Implementation in browser and HTTP header
By default, all major browsers, with the exception of Firefox, had an XSS filter and it was enabled by default. However, a HTTP header was introduced and supported by browsers to allow developers to alter the configuration of the security feature. The reason for this was one of compatibility.
The header was fairly simple with two attributes:
- Specifying if the filter is enabled - a simple Boolean value.
- Specifying the filter mode - filter or block.
If developers deemed their application sufficiently protected against XSS vulnerabilities, or their web application did not play well with a browser XSS filter, they could set the following header to disable the browser XSS filter:
- X-XSS-Protection: 0
However, if a developer wanted to utilise the browser’s XSS filter to protect its users, they could set the following:
- X-XSS-Protection: 1
The default mode, filter, for the header is arguably the weakest in terms of security. It attempts to neutralise the payload by filtering specific characters. As we will see later in this blog post, this mode introduced filtering of valid code, and XSS bypasses.
The other mode is block, which simply denies the request when a malicious payload is detected:
- X-XSS-Protection: 1; mode=block
It should be noted, that the default option for browsers varied over the years. For example, Chrome changed the default mode to block. Then, in 2019, there was a decision to change the default mode from block back to filter; which produced disastrous results.
Further information can be found at the following URLs:
XSS Filter in Action
Enough with the theory, let’s get our hands dirty!
Note: The browser versions that were used here still supported their XSS filters:
- Internet Explorer 11
- Version 77 Chrome (Removed v78 onwards)
Make sure the browser version you are using still supports its XSS filter or the examples below will not work.
Internet Explorer Filter Mode
Open Internet Explorer, from the homepage select the Labs drop-down menu and choose Reflected from the Server-side Subheading:
By default, the X-XSS-Protection header is applied by the web application and set to disabled, so a simple Cross-Site Scripting payload should work. Here’s an example payload: <svg/onload=prompt(9)>
Turn on the filter mode option by selecting the XSS Config drop-down menu and select Filter; then hit F5 to replay the request. You’ll notice that the XSS payload still triggered even though the XSS filter was enabled. What happened?
If you examine the HTTP request, you’ll realise that the referrer request header value matches the request URL. As mentioned earlier, if this comparison is true then the XSS filter permits the request.
This time, if you refresh the page by selecting the navigation input field and hitting enter, the request should be issued without the referrer header. The same XSS attack that worked earlier should now be filtered:
You can see a notification at the bottom of the page stating that a modification has be made to the page to prevent an XSS. If we view the source code, we can see that a character of our payload has been replaced with a hashtag.
Google Chrome Filter Mode
If we repeat this step in Chrome with the Filter enabled, we should see the following console error message.
Inside the source code, we can see the entire payload has been flagged and prevented from being executed.
Setting the XSS Config drop-down menu to Block, and sending the payload again produces the following:
Internet Explorer Block mode
Google Chrome Block Mode
Problems with X-XSS-Protection Filter Mode
Whilst the XSS filter is useful for neutralising malicious HTML, it can also be leveraged by an attacker to neutralise HTML that may enhance security. Open the following URL in Internet Explorer:
What happened here? The XSS filter identified valid HTML, an import of the BootStrap CSS, and neutralised it; hence the broken user interface.
Another flaw with browser XSS filters is multiple vulnerable injection points. Select the Labs drop-down menu and choose Reflected (multi) from the Extra subheading:
In the following example, the application permits two input fields to search for specific criteria:
Attempting a XSS payload in each field, such as <svg/onload=prompt(9)>, triggers the browser XSS filter. (make sure you refresh the request to ensure that it doesn’t have a matching referer header)
However, if we spread the payload across both fields, the browser XSS filter permits the attack:
Input 1: <svg
Input 2: onload=prompt(9)>
Digging into the DOM, we can see that the browser has interpreted the two separate input fields.
Death of XSS Filters
The goal of browser XSS filters was pretty virtuous, however, like most things in security (*cough* AngularJS Sandbox *cough*) the concept was flawed. As of recent, modern browsers have decided to supersede XSS filters with Content Security Policy (CSP).
- 19 March 2018: Frederik Braun closes the Firefox XSS filter development, this was determined internally in late 2016.
- 25 July 2018: Microsoft stated the retirement of the XSS Filter in Microsoft Edge.
- 05 August 2019: Google announce the deprecation and removal of XSS Auditor for Chrome 78.
- Only Internet Explorer still implements its XSS filter; Chrome, Firefox, and Edge do not.
- Browser XSS filters only protect against server-side reflected XSS attacks.
- Browser XSS filters only trigger if the referer header value does not match.
- Advise clients to implement CSP instead of browser XSS filters.
- Utilise Block mode; Filter mode can cause filter bypasses and attack legitimate code.
- Browser XSS filters don’t work for payloads spread over multiple input fields.