A Content Security Policy(CSP) is an important piece of mitigating multiple different types of attacks including XSS, Clickjacking, and other various types of code injections attacks. It does this by informing the end-users browser to not make requests or perform actions that don’t meet the CSP. Unfortunately, writing a generic CSP is not possible, and instead requires the developer to analyze their requirements and make appropriate exceptions before rolling out a CSP. In this article I will cover two sections: overall observations, and examples of Ruby on Rails specific gotcha’s I encountered along the way.

Overall Observations

My first step in writing a CSP was to enable the default one which Rails provides, and inspect what errors I received on the console. I was greeted with a few errors right away that looked very strange, which I quickly realized were not related to the application. They were caused by extensions which I had installed, specifically Vue Devtools and React Devtools. This lead me to my first takeaway: Use Private Browsing mode when testing your CSP to ensure the errors you are seeing are relevant!

Second, I noticed that the CSP messages in Firefox were not quite as helpful as the ones in Chrome. I ended up having a lot more luck tracking down the root cause of CSP issues in Chrome than in Firefox.

Rails-Specific Observations

There are 4 gems and 1 javascript code block I had to refactor as part of this exercise. I will include a link to my full set of changes at the end of the article since this was done on an open source project. Note that testing changes to your CSP in rails is slightly annoying since you need to reload the server after every change, since the CSP is saved in an initializer. If you feel like your changes aren’t taking effect, this is probably the reason.

Gems

Code Refactoring

CSP Settings

The default CSP in rails looks as follows (any items commented out were done by me):

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https

# Specify URI for violation reports
# policy.report_uri "/csp-violation-report-endpoint"
end

# If you are using UJS then enable automatic nonce generation
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

# Set the nonce only to specific directives
Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true

In this application, //= require jquery_ujs was included, so I went ahead and enabled the content_security_policy_nonce_generator feature. The only caveat was I also had to enable the content_security_policy_nonce_directives setting and limit it to only apply to script-src since all of the Chartkick inline styles broke and there was no straightforward way to pass the nonce to stylesheets.

Nonce Helpers

First of all, what is a nonce? A nonce is a one-time-use code which is changed every time the page is refreshed.

With the settings in the previous section enabled, it was necessary to modify the code in a few places to use the nonce. Specifically the following was the diff of the lines change in my application.html.haml:

+ = csp_meta_tag
- = javascript_include_tag "application"
+ = javascript_include_tag "application", nonce: true

The csp_meta_tag adds the a new tag to every page, and the nonce: true adds that same nonce to the javascript assets tag, for example:

<meta name="csp-nonce" content="Zp+yENSM5K9BKStdB22IbQ==" />
<script src="/assets/application-1798ca4812645a78f896665e43c690d14f965504736ca9fe705e2d59cce6d622.js" nonce="Zp+yENSM5K9BKStdB22IbQ=="></script>

This same nonce is accessible through the content_security_policy_nonce method and an example is used later on in the file.

Gems

Bootstrap 3

Bootstrap adds an inline style to navbar dropdowns. In my case the line:

%button.navbar-toggler{"aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-target" => "#navbarSupportedContent", "data-toggle" => "collapse", :type => "button"}

was throwing an error stating that the inline style

color: #fff

was not able to be applied. I was not applying a custom style to the navbar with an inline style, so this must be something that Bootstrap 3 adds on. Chartkick (below) also requires inline styles enabled, so I fixed this by ensuring the following was set in my CSP.

policy.style_src   :unsafe_inline, :self, :https

jQuery Rails

This application was relying on an old version of jQuery without realizing it. In our application.js there was an include for //= require jquery. This was causing an error inside jQuery itself on the following lines:

// Support: IE<9 (lack submit/change bubble), Firefox (lack focus(in | out) events)
for ( i in { submit: true, change: true, focusin: true } ) {
  eventName = "on" + i;

  if ( !( support[ i ] = eventName in window ) ) {

    // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)
    div.setAttribute( eventName, "t" );
    support[ i ] = div.attributes[ eventName ].expando === false;
  }
}

It turns out the jquery-rails gem includes jQuery 1, 2, and 3. I found that switching to //= require jquery2 or //= require jquery3 fixed the issue.

Chartkick

Chartkick has a really good writeup on what is required to make it work with CSP: chartkick CSP guide

In my case, I already had to enable unsafe_inline styles across the whole application due to Bootstrap 3, so this setting in conjunction with adding Chartkick.options[:nonce] = true to config/initializers/chartkick.rb solved all of my Chartkick issues.

reCAPTCHA

The documentation for reCAPTCHA states that whitelisting a few domains is required in order to allow reCAPTCHA to work with CSP enabled. This does not reflect what I experienced. I added invisible_recaptcha_tags nonce: content_security_policy_nonce (note that I am using the ambethia/recaptcha gem and afterwards reCAPTCHA worked fine for me. The nonce used here pulls in the same nonce that is added as described in the nonce helpers section above.

Code Refactoring

Rewriting inline scripts

There was one instance of an inline script which used data from the controller to show the time remaining countdown. In order to still pass the data needed into Javascript, I had to add the data to a data attribute. The transformation can be seen from here to here.

Resulting CSP

With all of my investigation completed, I found the following CSP worked best for the application:

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :unsafe_inline, :self, :https
end

# If you are using UJS then enable automatic nonce generation
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

# Set the nonce only to specific directives
Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

Relevant commit: mitre-cyber-academy/ctf-scoreboard