How to set up CSP and script-src correctly
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.
directly written in the HTML code of the page (i.e. inline scripts) and scripts
stored outside our domain, it's easy. Just use
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
<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
document.createElement('script'). Again, their
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
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.
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="...">
The Content Security Policy also prohibits the use of
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.
Almost the same thing that applies to
<script> can be
applied to cascading styles (
<style>) with the
style-src directive. The only
thing missing is
strict-dynamic, which applies only to
http: csp: style-src: [nonce, *, unsafe-inline]
Again, add the n:nonce attributes to the
Attributes onevent and style
There is no way to enable the use of attributes like
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.
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
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
Content-Security-Policy, and at the same time test their
Content-Security-Policy-Report-Only. Of course, you
can also have failures of live rules monitored.