DEV Community

Cover image for Protecting your frontend with a Content Security Policy
Jody Heavener
Jody Heavener

Posted on

Protecting your frontend with a Content Security Policy

Today we’re going to look at how a Content Security Policy can be configured to protect your web pages from Cross-Site Scripting attacks.

A primer on Cross-Site Scripting

Anyone who touches the front end of a web page should have at least a basic understanding of Cross-Site Scripting — and I mean anyone, whether you’re the React-SPA-“CSS in JS is life” front end ninja, the project manager who figured out how to open a PR to add yet another CTA modal, or the backender that just wants to render some simple HTML. If you’re not sure what Cross-Site Scripting (from here on out known as XSS) is, it’s time to get informed.

Flaming text that reads “U’ve been hAcKeD”

A very basic breakdown of XSS is that it’s a method with which someone executes unauthorized script(s) on a web page without the web page owners knowing or the user consenting. In some cases this could result in something silly, maybe the words “U’ve been hAcKeD” in big flaming letters across the screen, but it could also look a legitimate part of the page, perhaps by rendering what appears to be an official form that captures your sensitive information. There are a variety of other possible outcomes:

  • Stealing your cookies and other browser storage data, such as your session cookies.
  • Attempting to enable revealing browser APIs (geolocation, webcam). Sure you might be prompted, but when was the last time you dismissed a location request from a website?
  • Keylogging, mouse movement tracking, other session snooping.
  • Loading additional scripts to perform additional actions (hello crypto miners).

If you’re not entirely new to XSS you might be familiar with the three types of XSS that have long been the convention, Stored, Reflected, and DOM-based. Because these types actually can have quite a bit of overlap we’ve seen a new set of terms for XSS types come to prominence:

Server XSS occurs when someone is able to successful store nefarious data on the server, and when a user makes a request that results in the data being returned from the server and the browser renders it as HTML, allowing scripts to be called. This can be mitigated by sanitizing user output from the server (e.g. never rendering user output as HTML). It wouldn’t hurt to also sanitize it before it hits the server, but if it’s already in there your best bet is to catch it on the way out.

Client XSS occurs when an unsafe JavaScript call is used to update the DOM. The result is that the data being used in the call is treated as safe to execute, which allows scripts to be called. This could be the result of an AJAX call, a value from browser storage, or some other in-DOM method. This can be mitigated by not using unsafe JavaScript (e.g. document.write, el.innerHTML, eval), but you’ll see below why even these measures can be tricky to implement.

Just in case you had any doubts about whether or not XSS is a real problem, have a look at these findings:

  • The Open Web Application Security Project lists XSS as one of the Top Ten security risks to web applications (as of 2017).
  • The Edgescan Vulnerability Stats Report 2019 notes “[XSS], both reflected and stored, was the most common vulnerability in 2018 at 14.69%”.
  • Noted in HackerOne’s The 2019 Hacker Report, “over 38% of hackers surveyed said they prefer searching for cross-site scripting vulnerabilities” when asked about their favourite attack methods and vectors.

How would a Content Security Policy help?

Content-Security-Policy (CSP) is one of the fundamental controls for XSS prevention. You can generally set it up once and it’ll look out for you until the end of days.

Let’s look at it from this angle: XSS is hard to prevent. You don’t really see what’s going on because it can all occur on the client side, doesn’t necessarily have to hit the server, and when it does it might not be so easy to detect. So it helps to have a lot of different security controls. As I noted above, this can include input validation and output encoding.

Output encoding is of course a valid method to help prevent against XSS attacks, but it is not always reliable. Let’s use output that is used in HTML as an example:

<a href="{{ output }}">Some link</a>
Enter fullscreen mode Exit fullscreen mode

In the above example, if output is <script>alert(1)</script> and it is rendered without escaping the HTML characters, the JavaScript would execute. However if you escaped them, this would be fine to print in the HTML. But, what if it’s not an HTML context? What if output is javascript:alert(1), which is another valid form of executing JavaScript in various HTML attributes. Now that we’re in a JavaScript context escaping HTML would do nothing here and the code would run as designed.

This was just one example, but it’s meant to emphasize how important it is to think about the context in which your output is printed and executed. You might be convinced that your user output is covered, or you might not even have user-generated output at this point in your application’s life, but a CSP is something that can help you now (everyone makes mistakes) as well as help future-proof your product.

Enter CSP

In essence a Content Security Policy (CSP) is a set of rules that tell the browser which types of files or content can be loaded on the page and in what contexts, and anything that does not match that criteria will be discarded. This helps prevent XSS payloads from being rendered on the page. If the browser supports CSP it’ll enforce it, if not it doesn’t harm anything.

There are many types of files or content that can be allowed or restricted using a CSP, including scripts, styles, fonts, manifests, media (video, audio), embedded frames, and more.

A CSP can be set two ways. Either by the web server, where it is sent over via an HTTP header, or by a meta tag in the page’s markup. Here’s an example that restricts loading all resources to HTTPS:

// header
Content-Security-Policy: default-src https:

// meta tag
<meta http-equiv="Content-Security-Policy" content="default-src https:">
Enter fullscreen mode Exit fullscreen mode

So think back to the example in the previous section. If we were to set our CSP to default-src 'self'; script-src 'self' and tried to run the above inline JavaScript context, it would fail. Why? Because this policy does not allow inline execution of JavaScript.

If you need help building a custom CSP you can use CSP is Awesome, and if you want to analyze another website’s CSP, check out this tool from Report URI.

Here are some more examples, from the Content Security Policy Reference:

// Allow everything but only from the same origin
default-src 'self';
// Only Allow Scripts from the same origin
script-src 'self';
// Allow Google Analytics, Google AJAX CDN and Same Origin
script-src 'self';
// Allow images, scripts, AJAX, and CSS from the same origin, and does not allow any other resources to load.
default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';
Enter fullscreen mode Exit fullscreen mode

Bypass vector

A common practice with CSP is to include wildcards URLs for common asset hosting origins.

An example, where you might want to load scripts from an AWS S3 bucket:

Content-Security-Policy: default-src 'self'; script-src 'self' *
Enter fullscreen mode Exit fullscreen mode

If someone were to discover that your site is vulnerable to XSS, all they would have to do is put their script in an S3 bucket and use that address to load it on your web site.

Using nonces with your CSPs

CSPs support restricting the execution of scripts with a nonce.

If you’re not sure what a nonce is, it’s just a single-use set of characters (number used once) generated by the server with which you can use to verify that a request is legitimate. Often this involves validating the nonce from one request to another on the server, but in this case we're actually going just validate it in the browser.

As an example, let’s say your server generates the nonce value of 123456. You would then set a CSP header with that value:

Content-Security-Policy: default-src 'self'; script-src 'nonce-123456'
Enter fullscreen mode Exit fullscreen mode

Now, when you render a script you would also use this same nonce value as the nonce attribute on the script tag:

<!-- these would be discarded -->
<script>document.write('super haxed');</script>
<script nonce="123457">document.body.innerHTML = '<h1>Got u!!!</h1>';</script>

<!-- this would execute -->
<script nonce="123456">console.log('hello!')</script> 
Enter fullscreen mode Exit fullscreen mode

The browser will then check the header value against the script tag value; if they don't match the script will not execute.

Reporting XSS attacks

Not only can a CSP help stop intruders, but it can also narc on them.

The browser that a CSP violation occurs on can send a JSON report (via POST) to a specified URI for you to then analyze. Here’s what this might look like:

Content-Security-Policy: default-src 'self'; report-uri
Enter fullscreen mode Exit fullscreen mode

Note this uses the report-to directive, and while a handful of browsers do support this it’s no longer the recommended method. The new directive is report-to, but as of writing it’s only supported by about 66% of browser users, so it’s best to include both directives in your policy.

Wrapping up

I hope this helped shine a light on how important it is to protect against XSS attacks, and how a Content Security Policy can greatly help with this.

If you’re still curious about this topic and want to learn more, here are some fantastic posts from others in the DEV community:

Additionally, here are some of the resources that I used to help develop this post (🙏):

Top comments (0)