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.

Theheaderwilllookasfollowsifyouwanttoallowscripts(script-src)onlyinexternalfilesfromthesameorigin(’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:Allowedsourcesfor CSS.
  • default-src:Thefallbacksourceforbasicallyallsourcedirectives(*-src)ifthespecificsourceisn’tdefined.
As the standard evolves, it looks like this header will be used for all kinds ofsecurity-related policiesin the future:
  • block-all-mixed-content: Don’t load resources over HTTP if this is HTTPS
  • upgrade-insecure-requests: UseHTTPS if theHTTP 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 aContent-Security-Policy to adapt your Rails application to the future.

The Content Security Policy header field

The former X-Content-Security-PolicyandX-Webkit-CSPHTTPheaders are now deprecated. Going forwards you should only use theContent-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 2added 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 SecureHeadersgembecause 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 aContent-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 theContent-Security-Policy-Report-Only header and to allow scripts and styles only from the same origin.

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

  default_src:%w(*),#allallowedinthebeginning
  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
Bytheway,there’snoinheritancefromthedefaultsourcetotheothersourcedirectives.I.e.script-srcdoesn’tinherit*fromdefault-srcintheexampleabove.

A candidate default RailsContent Security Policy

Getting a general CSP for the masses right is complicated. This Rails pull requesttries to do that, so let’s compare what’s different to the oneabove:
...
  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’sbecause 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 andrate-limitthe 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 addhttps://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:
  • Includescriptsandvariabledatain*.html.erbfiles(orintoHAML,Slimforthatmatter)
  • Include scripts in *.html.erb files, but source variable data through your own JSON API
  • Completely divide markup, variable data, and scripts
  • Divideitinto2groups:Markup+variabledataandscripts
The first 2 options won’t be possible for CSP anymorebecausewe’ll need to move all scripts to external files to benefit from this new policy. Thethirdoption 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: Addjavascript_include_tagcontroller_nametothe layout template andcreateascript foreachcontrollerinapp/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:
<buttonclass='my-javascript-button'onclick="alert('hello');">

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

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

Reusing scripts

If you’ve used Rails’ unobtrusive jQuery adapterbefore, you know that scripts are especially useful if they can be reused. For example:<ahref="/users/5"data-method="delete"rel="nofollow"data-confirm="Areyousure?">Delete</a> The script will asynchronously select all links with adata-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 theajax: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 Kit