A Content Security Policy (CSP) strategy

Rails Content Security PolicyA Content Security Policy comes in the form of an HTTP header and declares rules for what sources are allowed for all sorts of assets. In consequence, everything else is disallowed. If implemented well, it will completely wipe out all Cross-Site-Scripting (XSS) vulnerabilities in your app. Why? Because CSP effectively disallows inline JavaScript and CSS.

The header will look as follows if you want to allow scripts (script-src) only in external files from the same origin (’self’):

Content-Security-Policy: script-src 'self';
The script-src part is a so-called directive, the following is its’ value. Every policy is separated by a semicolon. The 3 most important directives are:
  • script-src: Allowed origins for scripts.
  • style-src: Allowed sources for CSS.
  • default-src: The fallback source for basically all source directives (*-src) if the specific source isn’t defined.
As the standard evolves, it looks like this header will be used for all kinds of security-related policies in the future:
  • block-all-mixed-content: Don’t load resources over HTTP if this is HTTPS
  • upgrade-insecure-requests: Use HTTPS if the HTTP version of a resource was requested
Reference:

Support rate

The Content Security Policy is supported by over 80% of today’s browsers. So it seems like a very good time to get started with a Content-Security-Policy to adapt your Rails application to the future.

The Content Security Policy header field

The former X-Content-Security-Policy and X-Webkit-CSP HTTP headers are now deprecated. Going forwards you should only use the Content-Security-Policy header. It’s currently supported by over 80% of today’s browser. Most of the directives were mentioned already in version 1 of the standard (80% browser support). Version 2 added a few features, but they aren’t supported by all browsers, yet.

Rails configuration recommendation

Adding an HTTP header in Rails is straightforward. But it’s recommended to use the SecureHeaders gem because it includes a feature to recognize the client’s user agent and send only the supported directives. If the browser doesn’t support CSP at all, the header won’t be sent. It wouldn’t do any harm to send it, but you will save a few bytes then.

Introduction strategy for a Content-Security-Policy

For applications slightly larger than a “Hello world“ application, you’ll need a strategy how to introduce this policy. Usually, there will be 2 parts:
  • Move all scripts and styles to external scripts and decouple scripts and markup completely (also in Ajax responses)
  • Start with a policy and use the introductory Content-Security-Policy-Report-Only header to receive violation reports at an endpoint and not block anything, yet. Then tweak the policy and slowly move to the real header

A CSP configuration to start with

You can use this basic SecureHeaders configuration in the beginning to send the Content-Security-Policy-Report-Only header and to allow scripts and styles only from the same origin.

 

config/initializers/csp.rb:
SecureHeaders::Configuration.default do |config|
  config.csp = {
    report_only: Rails.env.production?, # for the Content-Security-Policy-Report-Only header
    preserve_schemes: true, # default: false.

    default_src: %w(*), # all allowed in the beginning
    script_src: %w('self‘), # scripts only allowed in external files from the same origin
    connect_src: %w('self‘), # Ajax may connect only to the same origin
    style_src: %w('self' 'unsafe-inline‘), # styles only allowed in external files from the same origin and in style attributes (for now)
    report_uri: ["/csp_report?report_only=#{Rails.env.production?}“] # violation reports will be sent here
  }
end
By the way, there’s no inheritance from the default source to the other source directives. I.e. script-src doesn’t inherit * from default-src in the example above.

A candidate default Rails Content Security Policy

Getting a general CSP for the masses right is complicated. This Rails pull request tries to do that, so let’s compare what’s different to the one above:
...
    default_src: %w('self' https:), # the fallback is everything from the same origin and every HTTPS URL
    script_src: %w('self' https:), # see above
    font_src: %w('self' https: data:), # see above + data: resources
    img_src: %w('self' https: data:), # see above
    object_src: %w('none'), # no embedding
    style_src: %w('self' https: 'unsafe-inline'), # basically only styles from HTTP URLs or data: sources are disallowed
...
What’s different? Much less is allowed by default, but that’s because this one is targeted  at new applications. The one above is rather for seasoned applications. Many sources allow every HTTPS source, that’s pretty general, but a good starting point. I’d recommend investigating all exact source URLs and list only those if you can.

Violation reports

The first example included a report-uri directive which instructs the browser to POST JSON violation reports to that endpoint. See here for an example migration and controller in Rails. Go through the violation reports regularly to enhance the policy (or filter false positives).
However, take good caution with the endpoint and the reports. An attacker might forge a report to make you visit a certain site from the report or to run a DoS attack. You might want to require the user to be logged in and rate-limit the controller.

Are you using scripts from CDNs?

If you’re using CloudFlare, MaxCDN, Google’s or any other Content Delivery Network, you’ll have to add that URL to the allowed script sources. For example for jQuery from Google’s CDN add https://ajax.googleapis.com:
script_src: %w('self' https://ajax.googleapis.com)

Your JavaScript file organization

There are endless possibilities how to organize scripts, for both inline scripts (in the HTML), but also in external files:
  • Include scripts and variable data in *.html.erb files (or into HAML, Slim for that matter)
  • Include scripts in *.html.erb files, but source variable data through your own JSON API
  • Completely divide markup, variable data, and scripts
  • Divide it into 2 groups: Markup + variable data and scripts
The first 2 options won’t be possible for CSP anymore because we’ll need to move all scripts to external files to benefit from this new policy. The third option is great, but it requires very strict planning. In any case, the latter 2 options require that data and scripts are separated, for both normal and Ajax requests.
So where to put scripts if you’ve scripts inline in Erb files right now?
  • One script per controller: Add javascript_include_tag controller_name to the layout template and create a script for each controller in app/assets.
  • Or create one script for the entire application: This will possibly load a little slower for the very first request, but is much quicker for subsequent requests.

Move scripts to external files

Once you’ve decided for an architecture, you can start moving inline scripts out.

JavaScript events
Before:
<button class='my-javascript-button' onclick="alert('hello');">

 

After (HTML and script part):
<button class='my-javascript-button'>

$('.my-javascript-button').on('click', function() {
  alert('hello');
});

Reusing scripts

If you’ve used Rails’ unobtrusive jQuery adapter before, you know that scripts are especially useful if they can be reused. For example: <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a> The script will asynchronously select all links with a data-confirm attribute and show a message box when you click the link. If you can find common functionality in your app, try this approach.

Ajax scripts

If you’re using *.js.erb Ajax responses, unfortunately, you’ll have to move all those scripts to external files because jQuery uses eval() (an abbreviation for evil) to evaluate the response. This isn’t allowed with a Content-Security-Policy anymore unless you add a script-src 'unsafe-eval'. If you’re not sure how to do that: Make all Ajax responses only return markup/data and run pre-loaded scripts asynchronously on the client side once the Ajax response came back. For example by watching the ajax:success event.

Get started

The first step is to get the SecureHeaders gem.

Like this kind of articles?

Subscribe to hear about new Rails security resources first. Only helpful articles and guides. Monthly(ish) updates, no spam.

Unsubscribe at any time. Powered by ConvertKit