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
Would a code like below go unnoticed on your code review?
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-XSS-Protection), but there is a labour-intensive one -
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:
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
- Repeat steps 2-3 till all errors disappear.
Content-Security-Policy-Report-OnlyHTTP 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
- 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
- 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
OnBeforeResponsetill all errors disappear.
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
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
Ahead-of-Time (AOT) compilation (aka
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";
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.
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).