Javascript and Content Security Policy

Javascript and Content Security Policy

Introduction to CSP and how to use it

2022-01-16

The Content Security Policy (CSP) is an HTTP header that provides an added layer of security to web pages by informing the browser that certain insecure functionality should be disabled. For example, by limiting the ability of JavaScript code to run outside of a .js file on the same domain as the HTML page, we can prevent many attacks that involve injecting arbitrary JS code directly into the page.

Protecting frontend security with a CSP is important because with popular attacks like framejacking, XSS, CSRF, and more, attackers can steal sessions, alter content on a page, delete accounts, and more.

Before we dive into the specifics of how to see a website’s CSP, what the parts mean, and so on, let’s look at a CSP in action with real code. Imagine we have a Node app that takes an arbitrary im via a query string and renders it on a web page:

app.get('/loadImage', (req, res) => {
  const url = req.query.url;
  res.send(`<img src="${url}"></img>`)
})

This is insecure because the user can inject HTML, and therefore JavaScript code, which will execute on the page. This kind of attack is called Cross Site Scripting, or XSS.

To attack it, simply visit the URL http://localhost:3000/loadImage?url=%22%20onerror=alert(1)%3E while the app is runnng.

Now let’s see the CSP block this attack with a small change to our code:

Content Security Policy: The page’s settings blocked the loading of a resource at inline (“default-src”).

The browser blocks our exploit!

Because the CSP says default-src 'self', the browser will only run JavaScript loaded from a .js file on the same domain. Thus, even though our code is vulnerable, it’s not exploitable because the CSP blocks the unintended functionality. When we know that certain functionality is often required for certain kinds of exploits, we can avoid using that functionality in our code and restrict it in our CSP. This way, when the browser detects the functionality in use it can assume that it is malicious code that has been injected in our site, and block it accordingly.

However, it’s often the case that CSPs block what seems like normal JavaScript code. As a JS developer on a modern web application, it’s critical that you know how to locate a CSP and code appropriately so that you don’t violate the CSP, which will lead to broken functionality for the end user. In this article, we’ll go through each CSP directive that affects JavaScript code one by one and teach you how to modify your code to work within the confines and of the CSP and be more security conscious the same result in a more defense-minded way.

Caveat: If you want to study up on XSS before diving into the rest of the article, consider reading Cross Site Scripting (XSS) Software Attack | OWASP Foundation. Additionally, this article is not meant as a general introduction to CSPs. For that, check out Content Security Policy (CSP) - HTTP | MDN

Where to find the CSP

In the code snippet from the intro, we serve the CSP via an HTTP header. That is the usual way, but not the only way. If you need to debug that’s rejected by the CSP, you’ll need to know how to find the CSP so you can see which directives it’s using. Let’s go over the three most common places you’ll find it:

  • HTTP header

As mentioned before, this is the conventional place to put it. Use your browser’s networking tab to look at the header, or use a command line utility like curl. For example, we can use curl to see what the University of Constantinople’s CSP looks like

curl -s -I constantinople.edu.gr | grep --ignore-case 'Content-Security-Policy'
Content-Security-Policy: default-src 'none'

This is the easiest way to use a CSP, because you can configure the header in the webserver so it is automatically deployed to all pages.

  • meta tag

Sometimes, a certain page needs specific protections that differ from the others. In these cases, you /could/ configure the webserver to serve a different CSP just for that page, and in fact, that is a common and perfectly acceptible solution.

But another option is to serve the CSP via a <meta> tag. So in addition to checking the Content-Security-Policy HTTP header, be sure to check whether there’s a CSP served via a <meta> tag. Especially since <meta> based CSPs override CSPs found in HTTP headers.

For example, we can look at the CSP of csplea.se, a firm specialized in HTTP header security, which is served in this way:

curl -s csplea.se | grep meta
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
  • manifest.json

Browser extensions set their CSP in their manifest, which can be accessed by extracting the raw extension. You usually won’t need to do this, but if you’re debugging a browser extension and can’t find the CSP, it’s probably because you need to look in this file.

Directives that govern JavaScript

Let’s get to the meat and potatoes of coding with a CSP - looking at individual directives and how to code for them.

  • script-src

This specifies what kind of sources JavaScript code can be run from. For example, in a .js file on any domain, on the same domain, or none at all.

If it is set to ‘none’, then you are out of luck. This indicates that JS is disallowed completely. To run JavaScript on this page, you’ll need to modify the CSP itself.

On the other hand, if it is set to the more common value of ‘self’, then you’ll be able to run JS code, but only in seperate JS files hosted on the same domain. So instead of:

<script>
   // your code here
</script>

You’ll need to follow a pattern like this:

<script src='code.js'></script>

This directive also usually blocks eval(), which is a bad practice anyway, unless it is set to unsafe-eval.

There is also a ‘nonce’ you can apply to allow inline script tags to be used, but only if they have the correct cryptographic nonce hash. This is coordinated on the backend. The idea is that the XSS attacker won’t know what nonce to use until the page is already rendered and it’s too late. It’s less secure than disallowing inline scripting entirely, but better than nothing for sure.

Packages like nonce-express - npm make this easy to implement on the backend. The following Node.js psuedocode might help you get an idea of how this works:

const { randomBytes } = require('crypto');

app.get('/blog', (req, res) => {
  const nonce = randomBytes(8).toString('hex');
  const csp = `script-src '${nonce}'`;
  res.add_header('Content-Security-Policy: ' + csp);
res.end(`<script nonce="${nonce}">yourCode()</script>`);
}

Not so intimidating, right? Just make sure your script is loading the nonce on the backend, and you will be good to go.

There is a similar feature called unsafe-hashes that allows webmasters to whitelist the hash of the contents of a script. This means you can code inline, but will need to ensure the webmaster gets the hash of your code and adds it to the CSP.

  • default-src

This works the same as script-src but applies to /all/ HTML elements with a src attribute as a backup in case a directive for a given class of elements is missing. This will only affect JS if there is no script-src directive present to override it.

<!-- This will also be blocked by default-src 'self' -->
<style>
 /* CSS code here */
</style>
  • connect-src

With this directive, admins can limit what external URLs can be called via JS. This can prevent cryptojacking, for example, where XSS is exploited to mine cryptocurrency in your users’ browsers, because the crypto pooling URL wouldn’t be whitelisted by this directive.

// calling an unapproved URL will be prevented with connect-src
axios.get('https://evil-domain.com');
// This prevents cryptojacking and any XSS who's payload calls an external API

To code around this directive, simply make sure that whatever URL you are calling is whitelisted in the CSP.

  • worker-src

Caveat: The worker-src directive doesn’t work on browser extensions. This is an open bug in current versions of Chrome and Firefox.

Authorizes sources for Worker, SharedWorker, or ServiceWorker scripts. The format is similar to script-src in that it supports ‘none’, ‘self’, ‘unsafe-inline’, and so on. That way, we can allow a URL for service workers specifically, without allowing it for all outgoing connections as a connect-src.

You just list valid sources and those are where web workers can be instantiated from. If such a directive is blocking your web workers, you likely just need to add the URL of the script they are launching from.


Those are the directives directly related to JS, but there are many, many more you can learn about from Mozilla’s full list: Content-Security-Policy - HTTP | MDN

Debugging CSP issues

If you find that the CSP /should’t/ be blocking your code, yet it is, consider checking the HTML head to see if a <meta> is overriding the HTTP CSP header.

CSPs also enable reporting via the report-uri and report-to directives, which allow admins to receive detailed reports of anytime a directive is violated. Sometimes this can give forwarning of a cyberattack in progress. But more often, it signals that some of your code is broken and violating a CSP directive, and therefore needs to be recoded according to the principles outlined above.

Mozilla has a complete guide to setting up reporting, if you haven’t already: CSP: report-uri - HTTP | MDN

Conclusion

By prohibiting insecure behavior not used by a web application, CSPs offer an additional layer of protection against script injections. XSS, however, is merely the beginning of what can be done with a CSP. Dozens of other attacks can be mitigated, and the many directives are well worth exploring even for JS devs not otherwise interested in security.

You don’t need to become a security engineer to analyze a CSP and see if anything could be improved. Automated tools such as the HTTP Security Evaluator from CSPlease will tell you everything that’s missing from your website’s CSP. It’s a lot cheaper than hiring an auditor.

Today’s devs have a variety of tools at their fingertips that make working within the sometimes arcane restrictions of security headers easier than ever before.