ponyfoo.com

Content-Security-Policy in Express apps

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

The Content-Security-Policy header is a bit frightening — will I break my website if I suddenly start blocking requests for external resources 😰? In this article we go over a gradual approach to adopting CSP, so that you can mitigate the risk of breaking your website while trying to make it more secure. 🔐

What is Content-Security-Policy?

CSP is an HTTP header that helps you mitigate XSS risk by preventing resources from untrusted origins from loading. CSP comes with several different directives, each of which serves a specific purpose. For instance, the img-src directive is used when loading images, script-src is used when loading scripts, connect-src is used for XHR, WebSocket and friends, and so on.

Each CSP directive lets you indicate which origins are trusted by using a whitelist-based approach. User agents which support CSP will avoid fetching resources that don’t match your server’s CSP directives. This means our server can determine, at a granular level, which origins are allowed for which kinds of resources.

For instance, you might serve all of your images through the imgur.com service, and thus you could set the header to Content-Security-Policy: "img-src: imgur.com;" and prevent images from any origins other than imgur.com from loading. In a similar fashion you could limit script loading to just a subdomain of your site, preventing XSS attacks from loading scripts from malicious.com or from anywhere else you haven’t proactively approved.

Content-Security-Policy in Express

If you’re using Express, it’s really simple to write maintainable CSP directives using helmet-csp. To implement the img-src rule we were talking about, we’d only have to write code link in the following snippet, and helmet-csp will take care of adding the appropriate header to our server’s HTTP responses.

const csp = require(`helmet-csp`)

app.use(csp({
  directives: {
    imgSrc: [`imgur.com`]
  }
}))

We could also use 'self' to allow image resources from the same origin. Note that 'self' must be in quotes.

app.use(csp({
  directives: {
    imgSrc: [`'self'`, `imgur.com`]
  }
}))

There is a directive called default-src that’s used as fallback for any undeclared directives. If we set default-src: 'self', for example, we’d allow scripts, styles, and every other kind of resource to be loaded from the same origin.

app.use(csp({
  directives: {
    defaultSrc: [`'self'`],
    imgSrc: [`'self'`, `imgur.com`]
  }
}))

Keep in mind that default-src isn’t inherited by other declared rules, meaning that if we now removed 'self' from img-src, only images from imgur.com would be allowed.

There are many different directives we can set. What follows is a table based off of MDN documentation, containing just the 15 most relevant rules. As you can see, we weren’t exaggerating when we said CSP allows us to determine where resources can be loaded from on a granular level. We can make our CSP header as complicated as we want it to be, with directives ranging from simple rules such as where images should be allowed to load from with img-src, to more severe ones like blocking all mixed content or restricting how the page works using the sandbox directive.

Directive Description
block-all-mixed-content Prevents loading any assets using HTTP when the page is loaded using HTTPS.
child-src Defines the valid sources for web workers and nested browsing contexts loaded using elements such as <frame> and <iframe>.
connect-src Restricts the URLs which can be loaded using script interfaces
default-src Serves as a fallback for the other fetch directives.
font-src Specifies valid sources for fonts loaded using @font-face.
frame-src Specifies valid sources for nested browsing contexts loading using elements such as <frame> and <iframe>.
img-src Specifies valid sources of images and favicons.
media-src Specifies valid sources for loading media using the <audio> and <video> elements.
object-src Specifies valid sources for the <object>, <embed>, and <applet> elements.
report-uri Instructs the user agent to report attempts to violate the Content Security Policy. These violation reports consist of JSON documents sent via an HTTP POST request to the specified URI.
require-sri-for Requires the use of SRI for scripts or styles on the page.
sandbox Enables a sandbox for the requested resource similar to the <iframe> sandbox attribute.
script-src Specifies valid sources for JavaScript.
style-src Specifies valid sources for stylesheets.
upgrade-insecure-requests Instructs user agents to treat all of a site’s insecure URLs (those served over HTTP) as though they have been replaced with secure URLs (those served over HTTPS). This directive is intended for web sites with large numbers of insecure legacy URLs that need to be rewritten.

Granted, the above table can feel intimidating. To get started with CSP, however, we don’t need to list every single rule. Generally speaking, a good idea is to set a default-src of 'self' and then whitelist other origins to allow specific resources to load. Using report-only is a great way of finding out what those other resources could be.

To get started, let’s use report-only

The Content-Security-Policy-Report-Only header is identical to the Content-Security-Policy header, except that it behaves like a dry run. The policy won’t be enforced, – resources will continue to load as they were – but the configured report-uri will be requested with a POST message and a JSON payload.

I’ll be using the winston logger to persist log messages for the CSP report, but you can use anything you’d like. Odds are, you already have some sort of logging solution in place – use that!

const winston = require(`winston`)

app.use(csp({
  directives: {
    defaultSrc: [`'self'`],
    reportUri: `/api/csp/report`
  },
  reportOnly: true
}))

app.post(`/api/csp/report`, (req, res) => {
  winston.warn(`CSP header violation`, req.body[`csp-report`])
  res.status(204).end()
})

Once you set up CSP reporting, your server will start logging reports of every single resource that would’ve been blocked by your CSP header. As the days pass, you’ll collect considerable amounts of data on the resources CSP would block.

You could fix the violations by relaxing your CSP directives, for example by adding origins you’ll allow images to be loaded from; or by changing how your site is implemented so that the resources that would have been blocked by the CSP directive aren’t requested anymore.

Keep adding directives to your CSP header without removing reportOnly: true from your helmet-csp configuration. As you tweak your header, you should see the amount of reports shrinking to a point where it’ll be safe to remove the reportOnly: true option. At this point, your CSP header will be in effect and requests for resources from untrusted origins will be blocked.

Generally speaking, there’s two hurdles to overcome when setting up a CSP header for production use.

Ads served by an ad network tend to load third party scripts, loading images, styles, and scripts from an assortment of different origins. Unless the ad network offers documentation on how to set up your CSP header to allow them to serve advertisements unimpeded, it can be hard to detect every origin casually in order to whitelist them. This is one of the reasons why the “report-only first” approach is recommended.

Inline scripts and inline styles can also be a hassle. Any occurrence of onclick handlers directly in your HTML code, inline script tags such as your typical Google Analytics snippet, or inline styles such as those recommended to optimize critical rendering path performance will be blocked by CSP unless you add the shame-inducing 'unsafe-inline' source to script-src and style-src directives, respectively. The same goes with'unsafe-eval', which whitelists JavaScript code such as eval(`code`), setTimeout(`code`) or new Function(`code`)._

Inlining safely using a nonce

You probably have inline styles and scripts like the Google Analytics snippet we’ve mentioned above. To whitelist these snippets without turning on the 'unsafe-inline' rule, we can use a nonce. A nonce is an unique code we generate for every request, using a module such as uuid.

In the following piece of code, we generate a nonce for every request.

const uuid = require(`uuid`)

app.use((req, res, next) => {
  res.locals.nonce = uuid.v4()
  next()
})

We then tell the CSP directive what the nonce for the current request is. Any inline scripts that don’t have the same nonce in a nonce attribute will be blocked.

const csp = require(`helmet-csp`)

app.use(csp({
  directives: {
    defaultSrc: [`'self'`],
    scriptSrc: [`'self'`, (req, res) => `'nonce-${ res.locals.nonce }'`]
  }
}))

Lastly, we apply the nonce as an attribute for any inline scripts and styles we have. This tells CSP-compliant browsers that the script is safe to execute.

// in the real world, this would be in your view engine:
app.use((req, res) => {
  res.end(`<script nonce='${ res.locals.nonce }'>alert('whitelisted!')</script>`)
})

You can repeat the process for inline styles, but using style-src and <style nonce='...'>.

Closing the loop with upgrade-insecure-requests

Using CSP’s upgrade-insecure-requests directive, we could have the user agent automatically upgrade HTTP requests to HTTPS, effectively transforming code like this:

<img src='http://cats.com/hairy-cat.png' />

Into its secure counterpart:

<img src='https://cats.com/hairy-cat.png' />

Enabling this behavior in helmet-csp is merely a matter of setting upgradeInsecureRequests: true.

app.use(csp({
  directives: {
    upgradeInsecureRequests: true
  }
}))

CSP in Express, by Example

Here’s an example of how we compose the CSP header for Pony Foo. There’s quite a few whitelisted external services, and we weren’t able to completely get rid of inline styles, but it’s a good start!

const csp = require(`helmet-csp`)
const uuid = require(`uuid`)
const env = require(`./env`)
const authority = env(`AUTHORITY`)
const authorityIsSecure = authority.startsWith(`https`)
const authorityProtocol = authorityIsSecure ? `https` : `http`

function generateNonce(req, res, next) {
  const rhyphen = /-/g
  res.locals.nonce = uuid.v4().replace(rhyphen, ``)
  next()
}

function getNonce (req, res) {
  return `'nonce-${ res.locals.nonce }'`
}

function getDirectives () {
  const self = `'self'`
  const unsafeInline = `'unsafe-inline'`
  const scripts = [
    `https://www.google-analytics.com/`,
    `https://maps.googleapis.com/`,
    `https://static.getclicky.com/`,
    `https://in.getclicky.com/`,
    `https://cdn.carbonads.com/`,
    `http://srv.carbonads.net/`,
    `${ authorityProtocol }://adn.fusionads.net/`,
    `https://platform.twitter.com/`,
    `https://assets.codepen.io/`,
    `https://cdn.syndication.twimg.com/`
  ]
  const styles = [
    `https://fonts.googleapis.com/`,
    `https://platform.twitter.com/`
  ]
  const fonts = [
    `https://fonts.gstatic.com/`
  ]
  const frames = [
    `https://www.youtube.com/`,
    `https://speakerdeck.com/`,
    `https://player.vimeo.com/`,
    `https://syndication.twitter.com/`,
    `https://codepen.io/`
  ]
  const images = [
    `https:`,
    `data:`
  ]
  const connect = [
    `https://api.github.com/`,
    `https://maps.googleapis.com/`
  ]

  return {
    defaultSrc: [self],
    scriptSrc: [self, getNonce, ...scripts],
    styleSrc: [self, unsafeInline, ...styles],
    fontSrc: [self, ...fonts],
    frameSrc: [self, ...frames],
    connectSrc: [self, ...connect],
    imgSrc: [self, ...images],
    objectSrc: [self],

    // breaks pdf in chrome:
    // https://bugs.chromium.org/p/chromium/issues/detail?id=413851
    // sandbox: [`allow-forms`, `allow-scripts`, `allow-same-origin`],

    upgradeInsecureRequests: authorityIsSecure,
    reportUri: `/api/csp/report`
  }
}

app.use(generateNonce)
app.use(csp({
  directives: getDirectives()
}))

Further Reading

If you liked this article, consider subscribing to Pony Foo Weekly, our periodic email newsletter on front-end development! 💌

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments