Alex KlausDeveloper  |  Architect  |  Leader
Preventing Cross-site scripting (XSS) attacks in Angular and React
06 June 2019

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

  1. Set the most restrictive policy:

    Content-Security-Policy-Report-Only: default-src 'self';
  2. Open the website and check out the error log in the browser’s console.

    Console log errors

  3. Tackle down the errors by adjusting the Content-Security-Policy-Report-Only policy.

  4. Repeat steps 2-3 till all errors disappear.

  5. Change Content-Security-Policy-Report-Only HTTP header to Content-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

  1. Set ”Decrypt HTTPS traffic” on the ”HTTPS” tab of ”Options” (it’ll install a TSL certificate). See Fiddler’s docs.

  2. Make custom changes to web responses, use FiddlerScript to add rules in OnBeforeResponse (see docs):

    Injecting HTTP headers in Fiddler

  3. Start capturing the traffic.

  4. Open the website in the browser along with the browser’s console and check the errors.

  5. 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).