Skip to content

Content Security Policy

We've started rolling out a new security feature called "Content Security Policy" or CSP. As a user, it will better protect your account against XSS attacks. But, be aware, it…

Author

We’ve started rolling out a new security feature called “Content Security Policy” or CSP. As a user, it will better protect your account against XSS attacks. But, be aware, it may cause issues with some browser extensions and bookmarklets.

Content Security Policy is a new HTTP header that provides a solid safety net against XSS attacks. It does this by blocking inline scripts and limiting the domains that other scripts can be loaded from. This doesn’t mean you can forget about escaping user data on the server side, but if you screw up, CSP will give you a last layer of defense.

Preparing your app

CSP header

Activating CSP in a Rails app is trivial since it’s just a simple header. You don’t need any separate libraries; a simple before filter should do.

before_filter :set_csp

def set_csp
  response.headers['Content-Security-Policy'] = "default-src *; script-src https://assets.example.com; style-src https://assets.example.com"
end

The header defines whitelisted urls that content can be loaded from. The script-src and style-src directives are both configured to our asset host’s (or CDNs) base URL. Then, no scripts can be loaded from hosts other than ours. Lastly, default-src is a catch-all for all other directives we didn’t define. For example, image-src and media-src can be used to restrict urls that images, video, and audio can loaded from.

If you want to broaden your browser support, set the same header value for X-Content-Security-Policy and X-WebKit-CSP as well. Going forward, you should only have to worry about the Content-Security-Policy standard.

As CSP implementations mature, this might become an out of the box feature built into Rails itself.

Turning on CSP is easy, getting your app CSP ready is the real challenge.

Inline scripts

Unless unsafe-inline is set, all inline script tags are blocked. This is the main protection you’ll want against XSS.

Most of our prior inline script usage was page specific configuration.

<script type="text/javascript">
GitHub.user = 'josh'
GitHub.repo = 'rails'
GitHub.branch = 'master'
</script>

A better place to put configuration like this would be in a relevant data-* attribute.

<div data-user="josh" data-repo="rails" data-branch="master">
</div>

Inline event handlers

Like inline script tags, inline event handlers are now out too.

If you’ve written any JS after 2008, you’ve probably used an unobtrusive style of attaching event handlers. But you may still have some inline handlers lurking around your codebase.

<a href="" onclick="handleClick();"></a>
<a href="javascript:handleClick();"></a>

Until Rails 3, Rails itself generated inline handlers for certain link_to and form_tag options.

<%= link_to "Delete", "/", :confirm => "Are you sure?" %>

would output

<a href="/" onclick="return confirm('Are you sure?');">Delete</a>

With Rails 3, it now emits a declarative data attribute.

<a href="/" data-confirm="Are you sure?">Delete</a>

You’ll need to be using a UJS driver like jquery-ujs or rails-behaviors for these data attributes to have any effect.

Eval

The use of eval() is also disabled unless unsafe-eval is set.

Though you may not be using eval() directly in your app code, if you are using any sort of client side templating library, it might be. Typically string templates are parsed and compiled into JS functions which are evaled on the client side for better performance. Take @jeresig‘s classic micro-templating script for an example. A better approach would be precompiling these templates on the server side using a library like sstephenson/ruby-ejs.

Another gotcha is returning JavaScript from the server side via RJS or a “.js.erb” template. These would be actions using format.js in a respond_to block. Both jQuery and Prototype need to use eval() to run this code from the XHR response. It’s unfortunate that this doesn’t work, since your own server is white listed in the script-src directive. Browsers would need native support for evaluating text/javascript bodies in order to enforce the CSP policy correctly.

Inline CSS

Unless unsafe-inline is set on style-src, all inline style attributes are blocked.

The most common use case is to hide an element on load.

<div class="tab"></div>
<div class="tab" style="display:none"></div>
<div class="tab" style="display:none"></div>

A better approach here would be using a CSS state class.

<div class="tab selected"></div>
<div class="tab"></div>
<div class="tab"></div>
tab { display: none }
tab.selected { display: block }

Though, there are caveats to actually using this feature. Libraries that do any sort of feature detection like jQuery or Modernizr typically generate and inject custom css into the page which sets off CSP alarms. So for now, most applications will probably need to just disable this feature.

Shortcomings

Bookmarklets

As made clear by the CSP spec, browser bookmarklets shouldn’t be affected by CSP.

Enforcing a CSP policy should not interfere with the operation of user-supplied scripts such as third-party user-agent add-ons and JavaScript bookmarklets.

http://www.w3.org/TR/CSP/#processing-model

Whenever the user agent would execute script contained in a javascript URI, instead the user agent must not execute the script. (The user agent should execute script contained in “bookmarklets” even when enforcing this restriction.)

http://www.w3.org/TR/CSP/#script-src

But, none of the browsers get this correct. All cause CSP violations and prevent the bookmarklet from functioning.

Though its highly discouraged, you can disable CSP in Firefox as a temporary workaround. Open up about:config and set security.csp.enable to false.

Extensions

As with bookmarklets, CSP isn’t supposed to interfere with any extensions either. But in reality, this isn’t always the case. Specifically, in Chrome and Safari, where extensions are built in JS themselves, its typical to make modifications to the current page which may trigger a CSP exception.

The Chrome LastPass extension has some issues with CSP compatibility since it attempts to inject inline <script> tags into the current document. We’ve contacted the LastPass developers about the issue.

CSSOM limitations

As part of the default CSP restrictions, inline CSS is disabled unless unsafe-inline is set on the style-src directive. At this time, only Chrome actually implements this restriction.

You can still dynamically change styles via the CSSOM.

The user agent is also not prevented from applying style from Cascading Style Sheets Object Model (CSSOM).

http://www.w3.org/TR/CSP/#style-src

This is pretty much a requirement if you intend to implement something like custom tooltips on your site which need to be dynamically absolutely positioned.

Though there still seems to be some bugs regarding inline style serialization.

An example of a specific bug is cloning an element with a style attribute.

var el = document.createElement('div');
el.style.display = 'none'
el.cloneNode(true);
> Refused to apply inline style because it violates the following Content Security Policy directive: "style-src http://localhost".

Also, as noted above, libraries that do feature detection like jQuery and Modernizr are going to trigger this exception as they generate and inject custom styles to test if they work. Hopefully, these issues can be resolved in the libraries themselves.

Reporting

The CSP reporting feature is actually a pretty neat idea. If an attacker found a legit XSS escaping bug on your site, victims with CSP enabled would report the violation back the server when they visit the page. This could act as sort of an XSS intrusion detection system.

However, because of the current state of bookmarklet and extension issues, most CSP violations are false positives that flood your reporting backend. Depending on the browser, the report payload can be pretty vague. You’re lucky to get a line number (without any offset) on a minified js file when a script triggers a violation. It’s usually impossible to tell if the error is happening in your JS or some extension inject code. This makes any sort of filtering impossible.

Conclusion

Even with these issues, we are still committing to rolling out CSP. Hopefully a wider CSP adoption helps smooth out these issues in the upcoming CSP 1.1 draft.

Also, special thanks to @mikewest at Google for helping us out.

Explore more from GitHub

Engineering

Engineering

Posts straight from the GitHub engineering team.
GitHub Universe 2024

GitHub Universe 2024

Get tickets to the 10th anniversary of our global developer event on AI, DevEx, and security.
GitHub Copilot

GitHub Copilot

Don't fly solo. Try 30 days for free.
Work at GitHub!

Work at GitHub!

Check out our current job openings.