How to set up CSP and script-src correctly

4 years ago by David Grudl  

Content Security Policy (CSP) is an additional security feature that tells the browser what other resources a page can load and how it can be displayed. This protects against the insertion of malicious code and attacks such as XSS. It is sent in the form of a header made up of a series of directives.

This article focuses on the script-src directive, which tells what scripts a page can execute. Its implementation is not entirely trivial.

If all we ask is that the browser prevents the execution of JavaScript directly written in the HTML code of the page (i.e. inline scripts) and scripts stored outside our domain, it's easy. Just use script-src 'self', or add another domain script-src 'self' static.mydomain.com.

But it's usually not that simple. Often we want to use libraries and tools outside our server, such as Google Analytics measurement code, advertising systems, captchas, etc. And here, unfortunately, the first version of CSP fails. It requires precise analysis of the content being loaded and setting the right rules. That is, to create a whitelist, listing all domains, which is not easy, as some scripts dynamically load other scripts from other domains, or are redirected to other domains, etc. Service providers often don't care about CSPs and rarely document the rules needed for their code to work. And even if you go to the trouble of creating the list by hand, you never know what might change in the future, so you have to constantly monitor the list to make sure it's still up to date and make corrections. Google's analysis has shown that even that careful tuning ultimately leads to you enabling such a broad scope that the whole point of the CSP falls down, you're just sending a much larger header with each request.

The next version of CSP level 2 (partially) replaces the whitelists with what's called a nonce, which is a randomly generated one-time token that differs for each HTTP request, sent in the script-src 'nonce-c7f26c' header and then included in each element:

<script nonce="c7f26c" src="..."></script>

<script nonce="c7f26c"> ... kód ... </script>

Scripts that do not have the correct nonce will be blocked by the browser. With the nonce finally approached CSP in the right way, unfortunately it doesn't work for other scripts that JavaScript dynamically inserts. Meaning an element created with document.createElement('script'). Again, their locations need to be enabled by whitelist or JavaScript needs to insert the nonce attribute into them.

This is only solved by CSP level 3. Finally! Just add strict-dynamic to the header. Unfortunately CSP 3 support in 2019 is far from ideal, it is missing in iOS for example.

How to set up script-src correctly?

We start with nonce & strict-dynamic. At the beginning of each request, we generate the nonce, send it in the header and write out the script in each element (examples below). This ensures that only signed scripts will run and the browser will block the others. In browsers that support CSP 3, we're all set, all libraries will work and nothing more needs to be done.

But browsers that only support CSP 2? Dynamic loading of additional scripts would not work there. And of course we don't want to laboriously examine where which library fetches what from, so we allow loading from anywhere by adding * (or the more strict 'https:' etc.) to the header, thus partially simulating strict-dynamic. Unfortunately, only partially, because the browser will block dynamic inline scripts, i.e. elements created with document.createElement('script') that contain code instead of the src attribute. CSP 2 cannot handle this. You can find out if this is the case with your site by monitoring, see below.

And what about browsers that only support CSP 1 and thus don't know nonce? As I wrote in the introduction, enumerating all domains is hardly a solvable task, which we certainly don't want to undergo for the sake of ancient little-used browsers, so we disable the protection in them, i.e. we allow loading from anywhere by the aforementioned asterisk, and also by enabling inline scripts (i.e. <script>...code...</script>) with unsafe-inline.

The resulting form of the script-src universal directive looks like this:

script-src 'nonce-XXXXX' 'strict-dynamic' * 'unsafe-inline'

Example of use in Nette

Since Nette has built-in support for CSP and nonce since version 2.4, you just need to specify in the configuration file:

http:
	csp:
		script-src: [nonce, strict-dynamic, *, unsafe-inline]

And then use in templates:

<script n:nonce src="...">

eval()

The Content Security Policy also prohibits the use of eval(). It is very unlikely that any of the libraries would use this function, but if they did, you can enable it by adding 'unsafe-eval' to the header.

Cascading Styles

Almost the same thing that applies to <script> can be applied to cascading styles (<link> and <style>) with the style-src directive. The only thing missing is strict-dynamic, which applies only to JavaScript.

http:
	csp:
		style-src: [nonce, *, unsafe-inline]

Again, add the n:nonce attributes to the <style> or <link> elements.

Attributes onevent and style

There is no way to enable the use of attributes like onclick or style when using nonce, so they need to be rewritten into classic scripts or styles.

Images, iframes and others

Also for images, video, audio, iframes and similar you can use directives like img-src, media-src, frame-src, font-src or object-src to control where they are loaded from. But beware, for these directives the combination of nonce and asterisk behaves completely differently (i.e. nonce may not be supported) and you need to provide a whitelist of resources.

Monitoring

Before setting up new rules for CSP, try them out first using the Content-Security-Policy-Report-Only header. This works in all browsers that support CSP. If the rules are violated, the browser will not block the script, but just send a notification to the URL specified in the report-uri directive. To receive notifications and parse them, you can use a service such as Report URI.

http:
	cspReportOnly:
		script-src: [nonce, strict-dynamic, *, unsafe-inline]
		report-uri: https://xxx.report-uri.com/r/d/csp/reportOnly

This way you can detect if some library is failing on dynamic scripts in CSP 2, calling the eval function or some bug in the browser.

You can use both headers at the same time, and have validated and active rules in Content-Security-Policy, and at the same time test their modification in Content-Security-Policy-Report-Only. Of course, you can also have failures of live rules monitored.