Practical Prevention of Web Shenanigans With Content Security Policy

Use CSP to help protect your site

July 12, 2021
We're Expedited Security. We help SAAS applications prevent and recover from attack, overcome regulatory or integration security requirements or just stop "weird" traffic before it becomes a problem.

An attacker visits a site and registers a <script> tag as their username. When a regular user visits the site and sees the attacker's username, the script tag is inserted into the page and the regular user's session is compromised by the attacker. The attacker can now grab passwords, payment information, and other private data from the regular user. Shenanigans occur.

This is Cross Site Scripting, or XSS.

Modern templating languages escape variables by default to prevent this, i.e.: {{ someVariable }} escapes someVariable by default. However:

A better solution is Content Security Policy (also known as BATSHIELD), created by Mozilla. It's a well supported HTTP header that says which origins the site allows for images, scripts, styles and other types of content. Anything else is blocked by the browser. If you're not familiar with CSP, read this excellent introduction from Mike West from the Chrome team (also known as my quest.

Our own site recently adopted CSP. Our goals were as follows:

  • Have a policy that was as tight as possible while still allowing our app to work.
  • Be able to to easily change requirements in the future.
  • Be able to easily identify sources of CSP violations.

The basics were easy:

  • The only inline scripts we used were from instructions specified by third party vendors. We consolidated them and replaced them with a single <script src="js/something.js">.
  • We used our backend templating to dynamically generate some inline styles. We replaced these with dynamic classes, and generated some CSS in our build system instead.

Some parts were more difficult: here's how we handled them.

CSP requirements for third party APIs

Third party APIs are a common part of the modern web. They're used for payment processing, font hosting, database as a service apps, showing happy tweets from customers, and a lot more besides.

So a large chunk of the work for CSP isn't in creating a policy for your own code: it's discovering, merging and managing policy requirements for third party APIs.

Our site uses the following third party APIs. Most did not have official documentation on CSP:

API Documented CSP Policy
Google Fonts No documented policy
Mixpanel No documented policy
Ractive.js Documented policy
Stripe Documented policy
Twitter oembed API No documented policy, but some CSP notes
Typekit Documented policy
Stormpath No documented policy

We patched the ractive docs ourselves shortly before this article. Otherwise, only Stripe and Typekit provided full policies, detailing the individual settings required for their services. Typekit uses inline styles for font events, but explained the requirement clearly in their documentation. We're happy to go without font events, however the CSP violation would still appear as on the console, so we've allowed inline styles until we've finished talking to Typekit.

Stripe specifies an exact, conservative policy, which is great, although they forgot a domain - they use q.stripe.com for error reporting. Their CSP requirements are also slightly hidden in an article about PCI DSS. On the other hand, their competitor Braintree just has a list of domains and wouldn't provide info about which domains are used for scripts, images etc. when asked.

So we built the CSP policy for each third party API, starting with official policies where possible, and then testing with CSP's report-only mode.

However we still wanted to keep the individual third party API policy requirements separate from our base policy. For example, we're moving from Google Fonts straight over to Typekit. When we get rid of Google Fonts, we want to be able to simply remove Google Fonts from our CSP policy.

We wanted something like this:

var policy = cspByAPI(basePolicy,
[
  'twitter',
  'mixpanel',
  'googleFonts',
  'stripe',
  'typekit',
  'ractive',
  'stormpath'
]);

I.e., produce a merged, sorted, non-redundant policy from the named entities. It didn't exist, so we wrote it.

It's not particularly earth-shattering code - however we do think it represents an excellent common-sense approach to handling CSP for modern websites. It should also save you a bunch of manual testing and research if you use any of the libraries mentioned.

Official policies are used wherever they're made available, and all are tested in a production app. Pull requests are welcome if you'd like to add policies for additional APIs, and if you'd like to port to Python / Ruby / Elixir / Go / whatever else you're more than welcome.

Including server variables in the browser

A common technique to include data from the backend into the front end is to include something like this in a backend template:

<script>
    var serverVars = {{{ serverVars }}}
<script>

Where {{{ serverVars }}} is a JSON-encoded set of variables.

Doing this with CSP requires the use of script-src unsafe-inline, which defeats a large part of the purpose of using CSP.

Instead, we made a non-executable <script> tag, eg:

In our backend template:

<script class="server-vars" type="application/x-configuration">
  {{{ serverVars }}}
</script>

The <script> tag here is not JavaScript, it's just data.

Then in a script tag on our server:

var serverVarsElement = document.querySelector('.server-vars')
if ( serverVarsElement ) {
    window.serverVars = JSON.parse(serverVarsElement.textContent);
}

This allowed us to get backend data into the frontend without inline JavaScript.

Handling CSP violation notifications

Browsers that encounter CSP violations will POST the details of the violation to the configured CSP report URI. We originally just logged these, then collected logs directly from Linux's journalctl, but a colleague mentioned report-uri.io, a new web service for collecting and summarizing CSP log information.

report-uri.io is new but very promising: it's a great way to:

  • watch the volume of CSP violations changing over time
  • see what's happened recently
  • see what our top policy violations are

With report-uri.io we were able to find things we'd missed in our original policy (StormPath uses bootstrap) as well as find common violations - now we've finished our policy, violations are mainly from browser extensions.

The future

Some thoughts after implementing CSP:

  • CSP is a slow, ongoing process. It starts by implementing report only, expanding the policy for any edge cases found, changing your app to enable tighter policies, enforcing CSP, and tightening further over time. While we've removed all inline scripts, we still use inline styles for Typekit and eval for Ractive. We're talking with Typekit to see what our options are: either font events that work with CSP, or being able to disable font events completely. Ractive is already looking at removing the eval requirement: there's a performance hit, but it may be worth it.
  • Seperating out libraries made it easy to understand why a given policy was implemented. It also helped us easily ditch Google Fonts when we decided to.
  • JavaScript library vendors - especially commercial ones - need to publish complete CSP docs in an obvious location. They're not doing that right now.
  • The summaries from report-uri.io were useful. We'd love to see this service grow over time and make more interesting violations more prominent: eg, someone injecting an unknown script is far more relevant than seeing 'Evernote' or 'Pinboard' browser extensions being blocked yet again.

We hope that was helpful and good luck with your CSP endeavors!