Content Security Policy for Single Page Web Apps

Deploying comprehensive CSP that supports template bootstrapping.

Written by Terrill Dent.

This week we fully enabled CSP on cash.me. It’s been months in the making and the result is a safer experience for all our customers. This is how we did it.

Implementing Content Security Policy (CSP) on an established website is like flipping over a rock while you’re on a hike, exposing a world of creepy crawlers that you know are there but don’t really expect. It surfaces all the connections, sources, redirects, iframes, and unexpected bad things too!

CSP is a new layer of defense

CSP is a set of rules, provided by the server, that instructs the browser which sources, destinations, and protocols are permitted for each type of resource. It allows you to restrict which domains images can be loaded from and JavaScript can connect to. You can also completely disallow inline scripts.

When used correctly CSP provides a second line of defense against cross site scripting vulnerabilities that happen despite our best efforts and practices. It can also protect customers from some types of malicious browser plugins, malware, and compromised JavaScript dependencies. And finally, it provides a way to report when bad things are happening.

In particular, CSP helps against certain types of browser malware which can compromise customer browsing without exploiting a flaw in the website itself. In a 24 hour period the Square Cash CSP policy blocks hundreds of script injections and many outgoing connection attempts that are caused by trackers and malware in infected browsers.

Square Cash CSP Events (24 hours)Effective Directive# Events Blockedscript-src1300frame-src400img-src300connect-src150object-src15

Implementing CSP

The purpose of CSP is to block bad things from happening. It’s best to take an iterative approach, deploying in stages, to avoid accidentally disabling part of your web app.

This is process we followed:

  1. Write a Policy
  2. Review Violations in Report Only mode
  3. Fix Violations or Adjust Policy 
    … repeating 1–3 until violations are truly exceptional …
  4. Deploy in Strict Mode

1) Write a Policy

Start by listing source domains (with protocols) that you expect your application to load resources from or connect to. Doing this first allows you to start with a strict policy that doesn’t have many exceptions. Later you can add things that can’t be fixed.

Below is a sample policy that has been annotated. It is based on the Square Cash policy:

default-src                   // Rules applied to any directives not listed below
'self' // Can access resources on the host domain
https://cash.squarecdn.com; // Can access the CDN
style-src                     // Explicitly defined directives do not inherit from `default-src`
'self' // We must re-state everything that should be allowed
'unsafe-inline' // Allow inline CSS styles
https://cash.squarecdn.com; // Allow CSS from the CDN
img-src
'self'
data: // Allow data URIs (inline images)
https://cash.squarecdn.com
https://images.squareup.com
https://www.facebook.com; // Facebook Connect Library
frame-src
'self'
https://www.facebook.com // Facebook Connect Library
https://www.google.com // For Google Remarketing Code
https://www.google.ca // For Google Remarketing Code
squarecash:; // Allow iframes to open Cash iOS App URLs (deep linking to app)
script-src
'self'
'nonce-YLMZop38Ktla8/hmmA==' // Script Nonce. For inline <script> tags
'unsafe-inline' // For Safari, Allow inline scripts initially, we turn them off later
https://cash.squarecdn.com
https://connect.facebook.net // Facebook Connect Library
https://ajax.googleapis.com
https://www.google-analytics.com
https://www.googleadservices.com
squarecash:; // Also required for iOS App Deep Linking
report-uri
/event/csp-report // Path where Violation Reports are sent

A strong default-src is important because it represents any directives that are not explicitly defined. In the example above default-src will be allied to the missing font-src, object-src, media-src and any other directives not explicitly listed. The policy contributes to total response size on every HTML response. Smaller is faster, and fewer definitions are easier to understand.

Tip: Avoid using wildcard domains unless you are confident that all subdomains are secured. If an attacker can create a new subdomain and host malicious content there, they can bypass CSP protections.

2) Reporting Violations

Deploying the initial policy with the HTTP header Content-Security-Policy-Report-Only instructs the browser to report the violations but also them to execute. Violations are reported to the URL defined in the report-uri section. This lets us collect information about violations and fix issues incrementally.

Depending on your analytics framework, you may want to listen for securitypolicyviolation events with JavaScript and collect more information about the client before reporting.

document.addEventListener('securitypolicyviolation', function(e) {
logEvent('csp-violation', {
'blocked-uri': e.blockedURI,
'document-uri': e.documentURI,
'effective-directive': e.effectiveDirective,
'original-policy': e.originalPolicy,
'referrer': e.referrer,
'violated-directive': e.violatedDirective
});
});

3) Fixing Violations

Fixing violations in the application can be tricky. Resist the urge to loosen the policy. Sometimes this means changing the way a feature works, or choosing a new library. Additionally, many popular dependencies present their own interesting policy requirements:

Angular and CSP
You need to opt-in when using CSP with AngularJS. Add the attribute directive ng-csp to your top level angular application tag and include the angular-csp.css style sheet.

Browser Extensions
To allow browser extensions to interact with the web page content include safari-extension:// and chrome-extension:// for the directives you’d like to allow.

Google Conversion Tracking
The Google Conversion Tracking JavaScript generates a series of redirects that end in the Google Top Level Domain of the local user (.ca .com .au, etc). It’s unfeasible to list all the google domains, and we can’t use a wildcard in that position. A compromise is to list countries you are most interested in.

iOS Native App Deep Linking URLs
Native app URLs have their own protocol. Square Cash uses squarecash://square.com/… to deep link into the app. We use the iframe deep linking technique and it requires adding the protocol squarecash: to both script-src and frame-src.

4) Deploying Strict Mode

There are two ways to deploy a policy: HTTP headers, and HTML <meta> tags. In order to get comprehensive coverage on target browsers Cash.me required both.

Headers are more effective than <meta> tags because they are applied immediately before any content can be modified. If both are present, the meta tag can be used to strengthen the headers policy, but it cannot add new domains as valid sources. We use this fact later to make Safari more secure. (More on multiple policies).

When you’re confident that the remaining Violation Reports are for things you want to block, change the header name to Content-Security-Policy. Violations will continue to be reported to the same report-uri, but will now be blocked from executing.

Nonces for Inline Scripts in Templates

When building a single page web app that bootstraps data in the template (like the sample below), we need at least one inline script to deliver the initial data payload. This is in direct competition with our desire to avoid specifying script-src unsafe-inline (because that is what stops most XSS attacks).

Fortunately there’s a way to bless individual script tags with a nonce. Nonces are server generated tokens applied to individual script tags, like this: <script nonce=”{{scriptNonce}}”>. Nonce values must be included in the script-src section of the CSP policy and can only be delivered in headers (they don’t work in meta tags).

Tip: Script nonces must be unique random values generated for each request. If they are guessable an attacker could predict the nonce and bypass your policy.

Safari Script Nonces Workaround

To work around Safari’s lack of support for script nonces in CSP Level 2, we serve a Content-Security-Policy header with the script-src directive that includes both a nonce and unsafe-inline. At first look this seems like an error, but luckily browsers that support nonces will see the nonce and ignore the unsafe-inline.

script-src
'self'
'nonce-YLMZop38Ktla8/hmmA==' // Script Nonce triggers strict mode in Firefox and Chrome
'unsafe-inline' // For Safari
https://cash.squarecdn.com;

We then serve a more strict version of the directive (omitting the ‘unsafe-inline’) into the HTML template where it can be immediately turned into a <meta> tag after the first inline script has been executed. This provides XSS protection against script injections from user-supplied content that gets generated by your application logic later.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="{{cdn}}/style.css">
</head>
<body>
<script nonce="{{scriptNonce}}">
// Bootstrapping JSON data for initial load
var bootstrappingData = {...}};
      // Script Directive from Server (without 'unsafe-inline')
var cspScriptPolicy = "script-src 'self' https://cash-f.squarecdn.com;";
      // Inject a <meta> tag to disable inline scripts
var cspMetaTag = document.createElement('meta');
cspMetaTag.setAttribute('http-equiv', 'Content-Security-Policy');
cspMetaTag.setAttribute('content', cspScriptPolicy);
document.getElementsByTagName('head')[0].appendChild(cspMetaTag);
</script>
<script src="{{cdn}}/app.js"></script>
</body>
</html>

Comprehensive Coverage

The technique above completes our support across the evergreen versions of all major browsers, and degrades gracefully on older browsers.

This is exactly the setup we hope for when building progressive web apps. The CSP deployment silently added a new layer of security that works today, without dropping support for older browsers.

Thanks to Daniele Perito and Sean Slinsky for their assistance!