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.
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.
Sign in to submit a comment