Why is XSS scary?
Cross-site scripting (XSS) is one of the most common application-layer web attacks. XSS vulnerabilities allow injection of malicious scripts into the web page, which get executed in the user’s web browser. And the result of such attack may not be as pretty as shown here:
Pretty, huh? Grab the entire script here, inject responsibly.
That loud video illustrates the prevalence of XSS vulnerabilities in websites of Dutch banks. It was presented by Brenno de Winter in 2015 at AppSecEU conference… He had a bit more fun in 2016… And not that much has changed in 2019.
Are Angular or React apps safe?
Yes… almost… to some degree.
Both Angular and React provide built-in input sanitization and treat all inputs as untrusted by default, which mitigates the main risk (see relevant docs for Angular and React). Though, with both frameworks you can shoot yourself in the foot by using either dangerouslySetInnerHTML in React or bypassSecurityTrust in Angular.
While mantra ”in input we don’t trust” works for SPA devs, don’t fall in a false sense of immunity to XSS attacks. There are other ways to inject client-side scripts into web pages. Also, inexperienced devs can be creative in writing non-secure code vulnerable to XSS attacks, e.g. add inline script or style, which can be easily leveraged by an attacker, or just have eval()
.
Would a code like below go unnoticed on your code review?
const website = "javascript:alert('Here you go! Hacked!');"
class MyPage extends React.Component {
render() {
return <a href={website}>Click here</a>
}
}
ReactDOM.render(<MyPage />, document.querySelector('#app'))
Check out other examples.
To take protection to another level we need proper HTTP headers.
HTTP headers to prevent Cross-site scripting (XSS)
Of course, you already run websites on HTTPS. Then scan your website with securityheaders.com to see HTTP headers you are missing. Likely, most of the required headers are easy to add (e.g. X-Frame-Options
or X-XSS-Protection
), but there is a labour-intensive one - Content-Security-Policy
.
Content Security Policy (CSP) defines approved sources for content on your site that the browser can load.
The realisation of complexity of CSP parameters may come after reviewing CSP Quick Reference Guide or MDN web docs. There are more than a dozen directives for granular tweaks and the most likely you need at least the following:
default-src: The default policy in case of a resource type dedicated directive is not defined,
script-src: Valid sources of JavaScript,
style-src: Valid sources of stylesheets,
img-src: Valid sources of images,
font-src: Valid sources of fonts,
report-uri: Instructs the browser to POST reports of policy failures to this URI
You can try an online CSP Builder tool to generate CSP policies.
How to apply CSP to existing website?
Figuring out the right CSP directives is a tedious process and requires quite a lot of testing. There are two different ways of doing it:
#1. Subtle way
Before diving in Content-Security-Policy
, start experiment with policies by monitoring (but not enforcing) their effects by setting Content-Security-Policy-Report-Only. It will attempt to POST
all violation reports to the specified URI (see report-uri directive) or write to the console if the report URI is not configured.
The steps would be
-
Set the most restrictive policy:
Content-Security-Policy-Report-Only: default-src 'self';
-
Open the website and check out the error log in the browser’s console.
-
Tackle down the errors by adjusting the
Content-Security-Policy-Report-Only
policy. -
Repeat steps 2-3 till all errors disappear.
-
Change
Content-Security-Policy-Report-Only
HTTP header toContent-Security-Policy
, so the defined policies are enforced.
For a quick turnaround in dev environment, you can experiment by setting not HTTP headers, but a Content-Security-Policy
meta tag:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'" />
Unfortunately, a meta tag for Content-Security-Policy-Report-Only
doesn’t exist (see feuture request).
#2. Hacky way
Feel adventurous? Use a web debugging proxy like Fiddler (for Windows) or Charles (for MacOS). They come in handy to simulate different policies straight on your production environment if it differs from the development environment.
Let’s take Fiddler for example. Your steps would be
-
Set ”Decrypt HTTPS traffic” on the ”HTTPS” tab of ”Options” (it’ll install a TSL certificate). See Fiddler’s docs.
-
Make custom changes to web responses, use FiddlerScript to add rules in
OnBeforeResponse
(see docs): -
Start capturing the traffic.
-
Open the website in the browser along with the browser’s console and check the errors.
-
Adjust the CSP settings in
OnBeforeResponse
till all errors disappear.
Reporting violations
Complex websites, where risks/costs of cutting off valid content are high, need notifications on policy violations. Fortunately, CSP has report-uri directive, which instructs the browser to POST JSON-formatted violation reports to a location specified in the report-uri
directive.
You don’t need to handle those reports on your own, report-uri.com does a good job and it’s free on a small scale.
Gotchas with CSP in Angular and React apps
Angular
Ahead-of-Time (AOT) compilation (aka ng build --prod
) separates out all JavaScript code from the index.html
file. Unfortunately, processing of the CSS is not as neat and styles remain inline in all the components (here is a ticket for tracking). So, we have to put up with unpleasant style-src 'unsafe-inline'
directive in the CSP headers and hope for the best.
For a simple app with Google fonts, the CSP header may look like
Content-Security-Policy: "default-src 'self'; style-src 'self' fonts.googleapis.com 'unsafe-inline'; font-src 'self' fonts.gstatic.com";
React
Create-react-app does a good job of preventing inline scripts and produces a more CSP-compatible HTML. Not by default though, you need INLINE_RUNTIME_CHUNK=false
setting in the .env
file (see docs).
But create-react-app
is not ideal, as it inserts small images directly into the HTML (ticket). So add the img-src data:
directive to the CSP.
This CSP header can be a good way to start:
Content-Security-Policy: "default-src 'self'; img-src data:";
Update (1st July): A related PR request has been merged and setting IMAGE_INLINE_SIZE_LIMIT=0
should eliminate inline base64 images. Though, I haven’t tried it yet, but if it works, create-react-app
becomes compliant with CSP.
Conclusion
Adding most of the HTTP headers is a quick win. Regarding the CSP, it does reduce risks of XSS-type attacks on modern browsers. If configured properly.
In 2016 Google published a paper called “CSP Is Dead, Long Live CSP!” It demonstrates that 95% of all CSP policies can be trivially bypassed by an attacker. How? No need to look too far, Angular apps require style-src 'unsafe-inline'
and they are not alone.
Add to that risks of cutting off some essential functionality by misconfiguring the CSP, days of tweaking the settings and you face a very lousy ROI.
As an alternative to whitelisting entire hosts, the Google guys suggested enabling individual scripts via an approach based on a combination of nonce
-based CSP with strict-dynamic
directive. Hence, if a script trusted with a nonce creates a new script at
runtime, this new script will also be considered legitimate.
Hmm… interesting. Though, nonce-based approach is not well-known yet, which means less documentation and more error-prone. Webpack is capable of adding nonce
to all scripts that it loads, but SPA frameworks are pending adoption (#3430, #12378).