Content Security Policy

In this lesson, we'll learn about one of the most powerful security features that browsers have introduced in recent years in mitigating Cross-site Scripting vulnerabilities and how it helps create a content trust policy.

As we discussed in the X-Frame-Options section, there are many attacks related to content injection in the user’s browser. These include Clickjacking attacks as well as another form of attacks known as Cross-Site-Scripting (XSS).

XSSXSS

Another improvement to the previous set of headers we reviewed so far is a header that can tell the browser which content to trust. This allows the browser to prevent attempts at content injection that is not trusted in the policy defined by the application owner.

With a Content Security Policy (CSP) it is possible to prevent a wide range of attacks, including Cross-site scripting and other content injections. The implementation of a CSP renders the use of the X-Frame-Options header obsolete.

The Risk

Using a Content Security Policy header will prevent and mitigate XSS and other injection attacks. Examples of some of the issues that can be prevented by setting a CSP policy include:

  • Inline JavaScript code specified with <script> tags, and any DOM events which trigger JavaScript execution such as onClick() etc.
  • Inline CSS code specified via a <style> tag or attribute elements.

The Solution

With CSP allowlists, we can allow many configurations for trusted content. As such, the initial setup can grow to a set of complex directives.

Let’s review one directive called connect-src. It is used to control which remote servers the browser is allowed to connect to via XMLHttpRequest (XHR), or <a> elements. Other script interfaces that are covered by this directive are: Fetch, WebSocket,EventSource, and Navigator.sendBeacon().

Acceptable values that we can set for this directive:

  • 'none' - not allowing remote calls such as XHR at all.
  • 'self' - only allow remote calls to our own domain (an exact domain/hostname - sub-domains aren’t allowed).

The following is an example of a content security policy being set. It allows the browser to make XHR requests to the website’s own domain and to Google’s API domain:

Content-Security-Policy: connect-src 'self' https://apis.google.com;

Another directive to control the allowlist for JavaScript sources is called script-src. This directive helps mitigate Cross-Site-Scripting (XSS) attacks by informing the browser which sources of content to trust when evaluating and executing JavaScript source code.

The script-src control supports the 'none' and 'self' keywords as values and includes the following options:

  • 'unsafe-inline': allow any inline JavaScript source code such as <script>, and DOM events triggering onClick() or javascript: URIs. It also affects CSS for inline tags.
  • 'unsafe-eval': allow execution of code using eval().

For example, the following is a policy for allowing JavaScript to be executed only from our own domain and from Google’s while allowing inline JavaScript code as well:

Content-Security-Policy: script-src 'self' https://apis.google.com 'unsafe-inline'

Note, the 'unsafe-inline' directive refers to a website’s own JavaScript sources.

A full list of supported directives can be found on the CSP policy directives page on MDN, but let’s cover some other common options and their values.

  • default-src: where a directive doesn’t have a value, it defaults to an open, non-restricting configuration. It is safer to set a default for all of the un-configured options, which is where the default-src directive comes in.
  • script-src: a directive to set which script sources we allow to load or execute JavaScript from. If it’s set to a value of 'self' then it will only allow sources from our own domain. Also, it will not allow inline JavaScript tags, such as <script>. To enable those, add 'unsafe-inline' too.

It should also be noted that when implementing CSP, the CSP configuration needs to meet the implementation of your web application architecture. If you deny inline <script> blocks, then your R&D team should be aware and well prepared for this. Otherwise, it may break features and functionality across code that depends on inline JavaScript code blocks.

Helmet Implementation

Using Helmet we can configure a secure policy for trusted content. Due to the potential for a complex configuration, we will review several different policies in smaller blocks of code to easily explain what is happening when we implement CSP.

The following Node.js code will add Helmet’s CSP middleware on each request so that the server responds with a CSP header and a simple security policy.

We define an allowlist in which JavaScript code and CSS resources are only allowed to load from the current origin, which is the exact hostname or domain (no sub-domains will be allowed):

const helmet = require("helmet");

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      scriptSrc: ["'self'"],
      styleSrc: ["'self'"],
    },
  })
);

It is important to remember that if no default policy is specified, then all other types of content policies are open and allowed, and some content policies which simply don’t have a default must be specified to be overridden.

Let’s construct the following content policy for our web application:

  • By default, allow resources to load only from our own domain origin or from our Amazon CDN. The defaultSrc refers to all script type sources, such as CSS, iframes, fonts, etc.
  • JavaScript sources are restricted to our own domain and Google’s hosted libraries domain so we can load AngularJS from Google.
  • Because our web application doesn’t need any kind of iframes embedding, we will disable such objects (refers to objectSrc and childSrc).
  • Forms should only be submitted to our own domain origin.
var helmet = require("helmet");

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'", "https://cdn.amazon.com"],
      scriptSrc: ["'self'", "https://ajax.googleapis.com"],
      childSrc: ["'none'"],
      objectSrc: ["'none'"],
      formAction: ["'none'"],
    },
  })
);

Gradual CSP Implementation

Your Content Security Policy will grow and change as your web application grows too. With the many varied directives, it could be challenging to introduce a policy all at once. Instead of touch-and-go enforcement, strive for an incremental approach.

The CSP header has a built-in directive that helps in understanding how your web application makes use of the content policy. This directive is used to track and report any actions performed by the browser that violate the content security policy.

It’s simple to add to any running web application:

Content-Security-Policy: default-src 'self'; report-uri https://mydomain.com/report

Note that the semicolon is added to end the content security policy directives, and begin a new report-uri directive.

Once added, the browser will send a POST request to the URI provided with a JSON format in the body for anything that violates the set Content Security Policy.

With Helmet’s csp middleware, this is easily configured:

const helmet = require("helmet");

app.use(
  helmet.csp({
    directives: {
      defaultSrc: ["self"],
      reportUri: "https://mydomain.com/report",
    },
  })
);

Another useful configuration for Helmet when we are still evaluating a Content Security Policy is to instruct the browser to only report on content policy violation and not block them:

const helmet = require("helmet");

app.use(
  helmet.csp({
    directives: {
      defaultSrc: ["self"],
      reportUri: "https://mydomain.com/report",
    },
    reportOnly: true,
  })
);