I'm currently working on improving the security of a Drupal 8 site by implementing a Content Security Policy. As this is still new to me, I would like to get some input on my strategy.
Relevant base setup
- Drupal 8, with a custom patch that makes
ckeditor.js
pass on nonces when loading it's plugins, CKEditor only being used by logged in users - Security Kit, patched to provide an alter hook for the CSP directives (https://www.drupal.org/project/seckit/issues/2844205#comment-14217383).
- Custom code to make modifications to the rendered HTML and to the CSP configuration from the Security Kit module
Preliminary considerations
I would like to not use 'unsafe-inline'
for scripts if possible, but I finally figured that this would only work for certain browsers.
I would also like to have an adequate CSP for different situations (different browsers, logged-in vs not logged-in).
This leads me to this idea for script-src
:
- using
nonce
and'strict-dynamic'
for browsers that support CSP v3 and non-cachable pages - using hashes for browsers that support CSP v3 and cachable pages
- using
'unsafe-inline'
together with a domain-based list for all other browsers
The main reason for having a browser distinction is that I couldn't find a way of creating the CSP directives backward-compatible without an unreasonable amount of changes to core and contrib modules.
What I have done so far on my local test site
I have added logic that uses a nonce
and 'strict-dynamic'
for chrome based browsers for logged in users, for which pages are not cached, assuring that the nonces are new and unique for every request. That logic is based on the User Agent string (which I know is unsafe, but don't see a better solution).
So basically, for chrome based browser, logged in users will get a CSP header with those script related CSP policies ('unsafe-inline'
in the script-src will be ignored by browsers that support 'strict-dynmic'
):
script-src 'self' 'unsafe-inline' *.googletagmanager.com *.google-analytics.com 'nonce-SECURENONCE' 'strict-dynamic';
script-src-attr 'unsafe-inline';
Anonymous users will get a CSP that looks roughly like this:
script-src 'self' 'unsafe-inline' *.googletagmanager.com *.google-analytics.com 'sha256-HASH_1' 'sha256-HASH_2' 'sha256-HASH_3' 'sha256-HASH_4' 'sha256-HASH_5' ...;
script-src-attr 'unsafe-inline';
Non chrome based browsers get a CSP header that looks like this:
script-src 'self' 'unsafe-inline' *.google-analytics.com *.googletagmanager.com;
I have also added logic, that, based on the above selection criteria, adds either nonce
attributes to every script tag (non-cached pages), or hash codes for every script tag (cached pages). This also allows me to have CKEditor working fine in the backend.
It seems to be working well on the different browsers I have tested with: Brave, Chrome, Edge, Firefox and Safari. Only the three former have a CSP that I would consider safe (also checked with https://csp-evaluator.withgoogle.com/).
Does that make sense as a general approach? Or is it flawed in a way that I start over from the beginning?
Main questions:
- Is it a valid approach to have a different CSP for logged-in vs anonymous users?
- Is it a valid approach to have different CSPs for different browsers (or different "reported browsers" actually)?