Using Content Security Policy (CSP) to Secure Web Applications

Category: Web Security Readings - Last Updated: Fri, 27 Mar 2020 - by Zbigniew Banach

Content Security Policy (CSP) is a computer security standard that provides an added layer of protection against Cross-Site Scripting (XSS), clickjacking, and other code injection attacks that rely on executing malicious content in the context of a trusted web page. By using suitable CSP directives in HTTP response headers, you can selectively specify which data sources should be permitted in your web application. This article shows how to use CSP headers to protect websites against XSS attacks and other attempts to bypass same-origin policy.

Content Security Policy

Why Do We Need CSP?

Web security is based on same-origin policy (SOP), which prevents a website from accessing data outside its own origin. In theory, this should be enough to ensure security, but the modern web requires sites to include lots of assets from external sources, such as scripts and other resources from content delivery networks (CDNs), Google Analytics scripts, fonts, styles, comment modules, social media buttons – the list goes on. 

At the same time, malicious hackers use cross-site scripting (XSS) attacks to trick websites trusted by the user into delivering malicious code. Without additional safety measures, the browser executes all code from a trusted origin and can’t tell which code is legitimate, so any injected malicious code is executed as well.

Enter Content Security Policy (CSP) – a standardized set of directives that tell the browser what content sources can be trusted and which should be blocked. Using carefully defined policies, you can restrict browser content to eliminate many common injection vectors and significantly reduce the risk of XSS attacks. By default, CSP also enforces modern script coding styles for extra security.

History and Browser Support

Content Security Policy is a candidate recommendation of the W3C working group on web application security. Version 1 (or Level 1) was proposed in 2012, with Level 2 following in 2014, and Level 3 in development since 2015 as a draft recommendation. While only a recommendation, CSP was quickly implemented by browser vendors, starting with Firefox 4. The vast majority of modern browsers support all or nearly all Level 2 directives, and this article describes CSP Level 2 as the de facto current standard.

CSP implementations have used 3 different content security policy header names, depending on the browser and time of adoption:

  • Content-Security-Policy: Standard header name recommended by W3C and used by all modern implementations (GoogleChrome since version 25, Firefox since version 23, Safari and other WebKit-based browsers since WebKit version 528). This is currently the only header to use.
  • X-WebKit-CSP (deprecated): Experimental header used in the past by Chrome and other WebKit-based browsers.
  • X-Content-Security-Policy (deprecated): Experimental header used in the past by browsers based on Gecko 2.

Using CSP Directives

CSP allows you to define a variety of content restrictions using directives, usually specified in HTTP response headers. Heres an example of adding CSP headers to an Apache web server:

Header set Content-Security-Policy "default-src 'self';"

Added to the httpd.conf or .htaccess file, this will set a default policy to allow only content from the current origin (see below for details).

If needed, you can also provide specific directives at page level using HTML meta tags. Here’s an example that sets the same policy as above:

<meta http-equiv="Content-Security-Policy" content="default-src 
    'self'">

Each directive consists of a name followed by one or more values and ends with a semicolon. You can use the * wildcard to match whole values, subdomains, schemes, and ports:

Content-Security-Policy: default-src *://*.example.com

This header would allow sources from any subdomain of example.com (but not example.com itself) using any scheme (http, https, etc.)

The official W3C recommendation contains a complete list of directives with more formal definitions, but the following overview should give you a good idea of the most commonly used ones.

Source Whitelist Directives

The main purpose of CSP is to restrict web content sources, so there are many directives for specifying permitted sources for various types of assets. Once a Content-Security-Policy header is specified, the browser will reject any content from sources that are not explicitly whitelisted using any of the directives below. Source values are separated by spaces and can include both URLs and the special keywords 'none', 'self', 'unsafe-inline', and 'unsafe-eval' (discussed in detail below). Note that each directive can be specified only once in the same header, and keywords must be entered in single quotes.

  • default-src is a fallback directive used to specify the default content policy for most of the source directives. Common uses include default-src 'self' to allow content from the current origin (but not its subdomains) and default-src 'none' to block everything that’s not explicitly whitelisted.
  • script-src is used to whitelist script sources. To allow scripts from the current origin only, use script-src 'self'.
  • style-src is used to whitelist CSS stylesheet sources. To allow stylesheets from the current origin only, use style-src 'self'.
  • connect-src specifies permitted origins for direct JavaScript connections that use EventSource, WebSocket, or XMLHttpRequest objects.
  • object-src allows control over the sources of plugins such as Flash. Note that you can also specify permitted plugin types using the plugin-types directive (unsupported in Firefox as of v76).
  • img-src lets you restrict image sources.
  • font-src specifies permitted sources for loading fonts.
  • media-src restricts origins for loading sound and video resources.
  • child-src is used to restrict permitted URLs for JavaScript workers and embedded frame contents, including embedded videos. In Level 3, frame-src and worker-src directives can be used instead to control embedded content and worker processes respectively.
  • frame-ancestors restricts URLs that can embed the current resource in <iframe>, <object> and similar elements.

Writing JavaScript and CSS with CSP in Mind

Inline code is a major injection vector because it always executes in the current context, so it can’t be restricted. When CSP is enabled, it blocks all inline code by default. This means no inline styles or inline scripts at all, including inline event handlers or javascript: URLs, so any new code should follow best practices to use external script and style files exclusively. The unsafe-inline keyword is available to allow inline code for all or some script sources, but the W3C recommends avoiding it where possible.

For example, an old-style HTML and JavaScript page might contain script code both in <script> tags and inline event handlers:

<script>function performButtonAction() {
   alert('You clicked the button!');}
</script>
<button onclick='performButtonAction();'>
   Want to click the button?
</button>

To refactor this to a modern and CSP-compliant style, put the script in a separate file and use HTML purely declaratively, so something like:

<!-- buttonexample.html -->
<script src='buttonexample.js'></script>
<button id='examplebutton'>Want to click the button?</button>

and

// buttonexample.js
function performButtonAction() {
   alert('You clicked the button!');
}
document.addEventListener('DOMContentLoaded', function () {
   document.getElementById('examplebutton')
      .addEventListener('click', performButtonAction);
});

Even after typical injection vectors have been blocked, attackers might still achieve script execution if dynamically evaluated code is used. This is why CSP also blocks all string evaluation functionality by default, including eval(), new Function(), setTimeout([string]), and similar constructs. This enforces several changes to coding practices, including the use of JSON.parse() instead of eval() for parsing JSON data.

Similar to unsafe-inline, you can use the unsafe-eval keyword to allow code evaluation for some or all origins. Again, this is against best practice for modern code, and should only be used for legacy code that can’t be refactored.

Nonces and Hashes to Allow Inline Scripts

If you absolutely need to make an exception for some legacy inline code that can’t be moved out to separate file, CSP provides two features to allow specific code blocks without resorting to unsafe-inline. To whitelist a specific piece of code, you can use either a nonce (unique one-time identifier) for a <script> tag or use a cryptographic hash for the code itself. You can then specify the nonce or hash in your script-src directive to allow that piece of inline code.

By definition, a nonce is a “number used once”, so it must be randomly generated for each page load. To use a script nonce, specify it in the <script> tag using the nonce attribute and also add it to the script-src directive, prefixing the value with nonce-. For example, you might have some legacy code:

<script nonce="uG2bsk6JIH923nsvp01n24KE">
   alert('Hello from Netsparker');
</script>

To allow this specific script tag, use:

Content-Security-Policy: script-src 'nonce-uG2bsk6JIH923nsvp01n24KE'

Regenerating the nonce for every page load can be troublesome, so another approach is to use a cryptographic hash of the permitted code itself. To do this, start by calculating the SHA hash of all characters inside the <script> tag (but without the tag itself). Then you can specify the hash value in the script-src directive, prefixing it with sha256-, sha384-, or sha512-, depending on the algorithm used. Here’s the same example, but without the nonce attribute:

<script>alert('Hello from Netsparker');</script>

The directive using the SHA256 hash of this code would then be:

Content-Security-Policy: script-src 'sha256-db9763638d4e260082532ed81baf740fe3589b0920ce6039233435abfdbc9ef7'

Page-level CSP Directives

Apart from whitelisting content sources, CSP can also enforce restrictions on the actions that the current page can take. To use this functionality, use the sandbox directive to treat the page as though it was inside a sandboxed iframe. For a full description of restrictions enforced by sandboxing a page, see Sandboxing in the HTML5 spec.

To improve security for older websites with lots of legacy HTTP pages, you can use the upgrade-insecure-requests directive to rewrite insecure URLs. This instructs user agents to change HTTP to HTTPS in URL schemes and can be invaluable when you still have multiple HTTP URLs.

Testing Policies and Monitoring Violations

Content Security Policy provides powerful functionality to control content sources and page behaviors. However, this also means that a single misconfigured directive could render a site partly or completely inaccessible to visitors, so you need a way to safely test directives. 

Before you go live with your CSP directives, you can use the Content-Security-Policy-Report-Only header instead of Content-Security-Policy. In report-only mode, the browser will monitor the policy and report violations but without actually enforcing the restrictions. Use the report-uri directive to tell the browser where it should post violation reports in JSON format. This can be any local or external URI, for example:

Content-Security-Policy-Report-Only: default-src 'self'; ...; 
    report-uri /your_csp_report_parser;

Note that you can combine Content-Security-Policy-Report-Only and Content-Security-Policy headers to test a new policy while still enforcing an existing one.

Once a policy is live, you can use the same report-uri directive to get detailed reports about policy violations. Each JSON report starts with the csp-report attribute and looks something like this:

{
  "csp-report": {
    "document-uri": "http://netsparker.com/index.html",
    "referrer": "http://nasty.example.com/",
    "blocked-uri": "http://nasty.example.com/nasty.js",
    "violated-directive": "script-src 'self' https://apis.google.com",
    "original-policy": "script-src 'self' https://apis.google.com; 
        report-uri http://netsparker.com/your_csp_report_parser"
  }
}

As you can see, the reports provide information about each policy violation, including the blocked URI and violated directive. This makes troubleshooting much easier, especially for policies with hundreds of directives and values.

Content Security Policy Support in Netsparker

During the last few years, CSP Level 2 has been implemented in all modern browsers and is widely used across the web as an effective way of reducing the risk of XSS. To reflect this, Netsparker checks for the presence of Content-Security-Policy HTTP headers and reports a “Best Practice” vulnerability if they are missing.

However, merely having the CSP header is not enough, as invalid directives will be ignored by browsers (and therefore ineffective), while unsafe directive values won't provide the expected level of protection. Netsparker runs over 20 detailed checks to ensure that directives use correct syntax combined with values that provide effective security. See the Vulnerability Index for a full list of CSP checks available in Netsparker.

Demo of Content Security Policy in Action

And finally, for a practical demonstration of configuring CSP headers, watch this Security Weekly interview with Netsparker security researcher Sven Morgenroth. Talking to Paul Asadoorian, Sven presents the problems that CSP is designed to solve and goes on to do a hands-on demonstration of CSP headers in action.

Netsparker

Keep up with the latest web security
content with weekly updates.