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

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/).

Is it a valid approach to have:

  1. different CSPs for logged-in vs anonymous users?
  2. different CSPs for different browsers (or different "reported browsers" actually)?
  • 1
    It feels like your main questions would apply to any website rather than a Drupal one in particular? Apologies if I'm wrong there. If they would, though, this might be better suited to (and more importantly get better answers on) Stack Overflow
    – Clive
    Sep 18, 2021 at 19:13
  • @Clive You might be right. I had hoped though that CSPs might be a topic for Drupal folks in general, and I also hoped that someone could confirm or dispute my approach in the specific context of Drupal.
    – berliner
    Sep 19, 2021 at 10:26

1 Answer 1

  1. Relying on the user agent is not reliable - for example I changed the user agent in my old browser, otherwise YouTube refuses to work. If, based on the results of the analysis of the violation reports, you change the CSP for such of my user agent, legitimate users of this browser may suffer.

  2. Using of script-src-attr 'unsafe-inline'; will open door for XSS in Chromium based browsers, because it allows event handlers in the tags (like onckick="alert('XSS')").

  3. Using of 'sha256-HASH' is not supported for external scripts in Safari/Firefox. If you do use it for inline <script> tags only, it is easier to use 'hash-value' for both logged and non-logged users (and not to use 'nonce').
    You can use mix 'nonce-' and 'sha256-' but in case of XSS 'nonce-' can be reused in the browser-cashed pages (this is difficult and unlikely).

Briefly, you can use CSP in browsers backward compatibility mode:

script-src 'self' 'strict-dynamic' *.googletagmanager.com *.google-analytics.com
  'sha256-HASH_1' 'sha256-HASH_2' ...;


script-src 'self' 'strict-dynamic' *.googletagmanager.com *.google-analytics.com 'nonce-SECURENONCE';
  • Chrome/Edge/Firefox supports 'strict-dinamic' (which cancels any host-based sources), therefore actual CSP will be script-src 'strict-dynamic' 'sha256-HASH_1' 'sha256-HASH_2' ...; or script-src 'strict-dynamic' 'nonce-SECURENONCE'; respectively.

  • Safari does not supports 'strict-dinamic', therefore actual CSP will be script-src 'self' *.googletagmanager.com *.google-analytics.com 'sha256-HASH_1' 'sha256-HASH_2' ...; or script-src 'self' *.googletagmanager.com *.google-analytics.com 'nonce-SECURENONCE'; respectively.

  • you do not need to use 'unsafe-inline' at all because it's cancelled by 'nonce-value' or 'hash-value' anyway. As for now there is not browsers that does not support 'nonce-value' / 'hash-value'.

The above CSP above does not allow an inline event handlers in the tags, but:

  1. CKEditor-5 does not requre 'unsafe-inline' and can be allowed with nonce/hash. The 'unsafe-inline' is required for CKEditor-4 only.

  2. Google Analytics does not require 'unsafe-inline' and can be allowed with nonce/hash.

  3. Google Tag Manager itself does not require 'unsafe-inline' and can be allowed with nonce/hash. The 'unsafe-inline' for GTM is required only if you use custom inline scripts in the "Custom HTML tags".
    Using a 'nonce-value' may be preferred because if the GTM script is inserted with the nonce= attribute, it will be redistributed to all scripts inserted by the Tag Manager, except "Custom HTML tags".

  4. If you use inline event handlers in the tags - an 'unsafe-inline' is mandatory so forget about 'nonces', 'hashes' and 'strict-dynamic' tokens. Since an 'unsafe-hashes' is not supported by Safari, you have no way safely allow inline event handlers in the tags.

  • Thanks for this extensive answer. I have doubts about the next steps though. My idea was to provide a maximum of security for modern browsers and not taking the low bar only because less modern browsers don't support it. Safari for example understands nonces, but doesn't understand strict-dynamic, which makes it impossible (in a Drupal context) to have a functioning backend. Hashes on the other hand don't allow CSP "inheritance" like nonces do with strict-dynamic (afaik). And in Drupal which still comes bundled with CKEditor 4, I honestly don't see a way of getting around 'unsafe-inline'.
    – berliner
    Sep 29, 2021 at 18:56
  • So is it inherently bad to provide different CSP rules based on the user agent even if it's not reliable? From my understanding, the low-bar CSP rules would apply always as a baseline, but user agents reporting as modern, would still get more secure rules. I don't see how that would create a problem.
    – berliner
    Sep 29, 2021 at 18:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.