Introduction

During my time at Gusto as a part of the Application Security team, I’ve been exploring ways to improve defense against Cross-Site Scripting (XSS) in modern web applications.

At Gusto, we primarily use Ruby on Rails and React.js. Individually each framework comes with some XSS protections out of the box, but sharing information between frameworks makes contextual output escaping or encoding more difficult. In other words, the what, when, where and how for escaping user input questions become more difficult to answer.

A Content Security Policy (CSP) allows us to define rules around content as another layer of defense in addition to sanitization and Rails’ and React’s built-in auto-escaping.

What is a Content Security Policy?

Content-Security-Policy (CSP) is an HTTP response header or a meta tag with a set of directives. The set of directives can be viewed as instructions for the browser on what type of content to trust and where and how such content can be sourced.

script-src
script-src directive with some host-source directives allowing for CSP bypass

For example, the script-src directive shown above allows us to define which host-sources the browser is able to load JavaScript files from. The browser will check the src attribute in a script tag against one of the host-sources listed and blocks the network request if the src attribute is not found.

enforcing_font-src
Network tab shows CSP enforcing "mode" blocking some fonts from loading

If you would like to learn more about CSP, this post by Mike West is a great place to get started! This post will focus on CSP as an HTTP response header.

What is the point of a Content Security Policy?

In theory, a strict CSP¹ enables an organization to follow the defense in depth security principle. However, in practice, a CSP is known to be painful to configure, deploy and maintain and a small mistake in configuration allows for CSP to be bypassed. If a deployed CSP is strict, it serves as another line of defense against XSS, clickjacking, CSS Exfiltration, and mixed content bugs.

In a Ruby on Rails application, CSP is an additional line of defense to proper sanitization of user input and Rails’ erb and HAML built-in auto HTML escaping. A CSP increases the cost of attack for an attacker because they would need to construct a payload that bypasses sanitization functions, and host-sources, nonces or hashes in the CSP configuration.

Besides reducing XSS risks, a CSP can provide protection against clickjacking with the frame-ancestors directive. A CSP can also reduce the attack surface for a CSS exfiltration attack by limiting an attacker's payload from making calls to hosts not whitelisted, sourcing malicious CSS documents, or injecting inline CSS.

defense_in_depth
Defense in depth - an attacker must break through each layer of defense from sanitization of user input to CSP in order to reach the crown jewels

Enforcing vs Report-Only

There are two "modes" for CSP - enforcing and report-only. Enforcing means all code that is in violation of the CSP will be blocked from loading. Report-Only does NOT block any code in violation of CSP from loading.

One might ask, what is the point of CSP in report-only "mode"? The purpose of report-only "mode" is to help the development process of CSP to be less painful as iterations on the policy will not break application functionality.

So how does one tell the difference between the two "modes"? If there is Content-Security-Policy HTTP response header, then CSP is in enforcing "mode". If there is a Content-Security-Policy-Report-Only HTTP response header, then CSP is in report-only "mode". Additionally, when violations occur in report-only "mode", CSP errors will include the keywords report only in the browser's developer tools console. For example, when these violations occur in Chrome, the error message in Chrome's DevTools console will be prepended with [Report-Only].

console-errors
Example of CSP violation in DevTools console when CSP is in Report-Only "mode"

However, it is possible to send both Content-Security-Policy and Content-Security-Policy-Report-Only headers. One use case of sending both headers is when an organization has an existing CSP in enforcing "mode", but wants to test new directives or configurations without breaking functionality. A Content-Security-Policy-Report-Only header can be sent with the new configuration in addition to the existing Content-Security-Policy header for testing the new changes before they are integrated into the existing CSP.

One way of checking which headers are set by the server is to make a request with curl and check the response headers.

curl -SL -D - https://example.com | grep Content-Security-Policy

Another way of checking headers is by using the Network tab in the browser's developer tools.

network-tab_headers
Network tab in Chrome DevTools shows that both Content-Security-Policy and Content-Security-Policy-Report-Only headers are sent

How Does Reporting Work?

When violations occur, CSP is capable of logging them through the report-uri/report-to directive.

report-uri_directive
Example report-uri directive

When the report-uri/report-to directive is set and a CSP violation occurs, the browser will make a POST request to the URL value corresponding to report-uri/report-to with a JSON body

An example of violation report

{
    "csp-report": {
        "blocked-uri": "inline",
        "column-number": 1,
        "document-uri": "http://gusto.com/path",
        "line-number": 21,
        "original-policy": "default-src 'self'; script-src 'unsafe-inline' 'strict-dynamic' 'nonce-[removed]'; report-uri https://subdomain.report-uri.com/r/d/csp/ReportOnly",
        "violated-directive": "script-src"
    }
}

Traditional CSP vs Nonce-based CSP

Traditional CSP is setting the directives as a whitelist of host-sources.

traditional-csp
Example of a directive in traditional CSP

However, a research paper published by Google suggests that the majority of traditional CSPs are ineffective in mitigating against XSS. Often web applications require a large amount of refactoring to remove inline scripts or styles. The keyword-source,'unsafe-inline', is a compromise to deploy CSP without breaking functionality in a production environment, but the use of 'unsafe-inline' effectively disables the protection CSP would have provided. To solve this problem, the paper recommends taking the nonce-based approach to implementing CSP for dynamic web applications.

A nonce is a randomly generated value that is not intended to be reused. A nonce-based CSP generates a base64 encoded nonce per each request then passes it through the HTTP response header and appends the nonce as an HTML attribute to all script and style tags. The nonces in all scripts and style tags are checked against the nonce in the response header. For all nonces in script and style tags that match the nonce in the header, the browser allows those scripts and styles to load during the browser’s subresource loading process.

nonce-based-script-src
Example of nonce-based CSP behavior with script-src directive

For those who are familiar with how CSRFs are mitigated, the CSRF or anti-forgery token embodies the same spirit as the nonce in CSP. Abstractly, the CSRF token and nonce are random values that are validated before an action is performed.


Special thanks to Fredrick Lee and Julianna Roen for their feedback on earlier versions of the post.

Appendix

[1] Google has some guidelines on what they believe what a strict configuration of a CSP looks like.