Support Content Security Policy

Created on 16 March 2021, over 3 years ago
Updated 13 September 2024, 2 months ago

Problem/Motivation

Drupal offers the Content Security Policy module to easily configure CSP for a site.

With CSP, Google recommends using tag manager with nonce:

The recommended way to do this is with a nonce, which should be an unguessable, random value that the server generates individually for each response. Supply the nonce value in the Content- Security-Policy script-src directive:

Content-Security-Policy: script-src 'nonce-{SERVER-GENERATED-NONCE}'; img-src www.googletagmanager.com

It would be great if this module could provide a way to automatically generate the nonce value to handle this.

This could also be done with a hash .

Feature request
Status

Needs review

Version

2.0

Component

Code

Created by

🇯🇵Japan ptmkenny

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇺🇸United States chrissnyder Maryland
  • 🇺🇸United States chrissnyder Maryland
  • 🇺🇸United States chrissnyder Maryland
  • 🇺🇸United States chrissnyder Maryland
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    54 pass
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    54 pass
  • 🇺🇸United States chrissnyder Maryland

    Attaching a patch file that matches the MR.

  • Status changed to Needs review about 1 year ago
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    54 pass
  • 🇺🇸United States chrissnyder Maryland
  • 🇨🇦Canada gapple

    As I understand it, the purpose of using a nonce in the tag manager snippet is to:
    1. allow inserting the snippet as an inline script element that's authorized by the page's policy
    2. propagate the nonce to additional scripts inserted by tag manager, so they are also authorized

    However, since google_tag uses a local wrapper script to do the actions of the tag manager snippet, uses document.createElement('script') to insert tag manager's script into the page, and AFAIK tag manager itself only directly controls additional scripts from the same domain - a nonce is not strictly necessary. Authorizing tag manager scripts with script-src 'self' https://www.googletagmanager.com; script-src-elem 'self' https://www.googletagmanager.com would (more or less¹) have the same effect as a nonce.
    Since a nonce value in a directive disables 'unsafe-inline' for that directive, which unfortunately is still somewhat commonly required, the google_tag module would need to allow by domain when a nonce cannot be applied anyways. I don't think the additional effort and potential integration challenges of selectively using a nonce are currently worthwhile over just authorizing tag manager's scripts by domain.

    ----
    ¹ there's some differences in what would be required for a policy bypass to potentially happen, and what could be accomplished with one - I'm not sure one is inherently better in this case though. A nonce with 'strict-dynamic' would be a different consideration, but would require every script on a page to have a nonce applied since it disables domain sources, and is currently difficult on a Drupal site without targeted site-specific effort.

  • 🇺🇸United States chrissnyder Maryland

    You are correct. The main goal of using a nonce for this module is the second reason, propagating the nonce to additional scripts inserted by tag manager, so they are also authorized.

    AFAIK tag manager itself only directly controls additional scripts from the same domain - a nonce is not strictly necessary.

    I was curious about this myself. The only thing I could find in Google's documentation is this line: "Tag Manager will then propagate the nonce to any scripts that it adds to the page."

    After some testing, I found that the nonce is propagated to other tags/scripts that Google Tag Manager adds to the page, even for tags from a different domain. I have attached a screenshot which illustrates this. (note that the value of the nonces is not visible in the screenshot because Chrome dev tools hides it)

    Since a nonce value in a directive disables 'unsafe-inline' for that directive, which unfortunately is still somewhat commonly required, the google_tag module would need to allow by domain when a nonce cannot be applied anyways.

    Some sites are able to avoid using unsafe-inline directives, and in that case, this nonce support would be beneficial. For example, I have projects that can avoid using 'unsafe-inline' on all unauthenticated requests (non-admin pages), and these are the pages where GTM scripts are added. Therefore, I think it would be beneficial to support adding a nonce for those who can leverage it.

  • 🇨🇦Canada gapple

    After some testing, I found that the nonce is propagated to other tags/scripts that Google Tag Manager adds to the page, even for tags from a different domain.

    That's good to know and good¹ for not requiring changes to the csp config to deploy changes from tag manager.

    I foresee the least potential "why isn't it working" issues from site builders with always adding domains to the policy, and then adding a nonce when possible. If a site doesn't use 'unsafe-inline' then the nonce will allow scripts from other domains and Just Work™️; if a site/page does require 'unsafe-inline' (so no nonce) then the main domain is still allowed, and csp violation reports should give enough direction for someone to add necessary extra domains to the csp config. Switching between only either domains or a nonce (or to force one option) could be difficult for site builders to understand and debug, but could be an option for advanced users².

    ----
    ¹ maybe arguably
    ² this is possibly an area where the csp module could provide an API for modules to just provide the domains, and a central option to control lenient/dynamic/strict behaviour for the policy.

  • 🇨🇦Canada gapple

    Some things I noticed with the current CSP Subscriber:
    - there's now a handy, if awkwardly named, helper function to handle checking fallback directives and merging in the sources if necessary.
    - csp module will automatically add domains from library definitions, but since google_tag is using a wrapper it can't pick up the domain that's necessary for script-src 🙈.
    - CspSubscriber.DOMAINS looks like it's targeting support of Google Analytics by default, but the documentation includes an additional domain than what's currently present for connect-src (it also wildcards the subdomains). There's also potential here to more selectively add the domains to the necessary directives.

    I've added another MR here to address these, but maybe it's worth a separate issue

  • Merge request !59Issue #3203811: Support Content Security Policy → (Closed) created by gapple
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    54 pass
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    Unable to generate test groups
  • 🇨🇦Canada gapple

    Some changes:

    • Merged domain changes into the nonce MR, so that pages with 'unsafe-inline' (and so no nonce) still work via allowed domains
    • CSP 1.22 now has a library for adding the nonce value to drupalSettings, so multiple modules won't each need to add it themselves
    • Changes to the name and structure of the fallbackAwareAppend method. It should now be more obvious why Csp::fallbackAwareAppendIfEnabled() isn't sufficient for appending the nonce. The structure changes made more sense as I was doing them - hopefully the code flow isn't confusing for others.
    • Added tests, cause I don't entirely trust myself otherwise

    A possibility is for fallbackAwareAppendNonce() to return a boolean value for if it actually does append the nonce value, which would then disable appending the domains to script-src and script-src-elem. As I mused in a previous comment, this dynamic behaviour could potentially be more confusing to site builders than always adding the domains though?

  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    Build Successful
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    Build Successful
  • 🇪🇸Spain facine

    Hi all, I came here looking for some information about nonce and GTM.

    According to https://developers.google.com/tag-platform/security/guides/csp, nonce should be generated for each response.

    The recommended method is to use a nonce, which should be an unguessable, random value that the server generates individually for each response.

    What about cached pages?

  • 🇨🇦Canada gapple

    What about cached pages?

    My understanding is that it's okay to resend a cached page with a nonce because the contents of the page are unchanged and nothing new can be injected into the page. If there's a means to change the content on the page, it should also break the cache and result in a new nonce value.

  • 🇪🇸Spain facine

    @greggles, that is great! Thanks for your help.

  • 🇺🇸United States greggles Denver, Colorado, USA

    @facine - I think thanks goes to @gapple, not me :)

  • 🇨🇦Canada gapple

    I've got a helper class/method for adding nonces that I think is ready for merging to the CSP module: Add helper for safely appending nonce/hash sources Fixed

    Something I want to check on, is that google_tag is currently still supporting core ^9.5 | ^10 while csp dev is currently dropping support for D9. Is it worth it for csp to include the new feature in a D9 compatible release to avoid upgrade conflicts for sites, or will google_tag be dropping D9 support for new releases soon as well?

  • 🇺🇸United States Guybrush Threepwood

    I applied the patch and am using latest stable CSP version with Drupal 10. I couldn't apply it using composer for some reason. Are there usage steps to make this patch work to make it so I don't have to use unsafe-inline?

  • 🇨🇦Canada gapple

    @Guybrush Threepwood
    There aren't any manual steps required after applying the patch.
    Google Tag Manager doesn't inherently require 'unsafe-inline', and doesn't need to add it if it can't use a nonce - it uses a nonce to authorize resources from additional external domains.

    If you're seeing 'unsafe-inline' in the policy header for your pages (and you haven't configured it in the CSP module settings) it is probably required by another module/library that the page is using.
    If you're getting warnings in the browser console that an inline script is blocked, then you probably have something else on the page that still requires 'unsafe-inline', but doesn't have a policy alter subscriber to apply it when needed. (or possibly you're including something through tag manager that adds inline script/style in a way that can't be authorized via a nonce).

  • 🇺🇸United States Guybrush Threepwood

    @gapple Thanks for the explanation. It was really helpful.

  • 🇳🇱Netherlands ricovandevin

    I have applied the patch and it seems to do what it needs to do. A nonce is calculated, passed in drupalSettings and applied to the GTM script. At least, I see a nonce attribute appearing. But I cannot check the value (also see comment #12). I can also see that injected scripts have nonce attributes, at least some of them. But my browser complains that it cannot execute the injected inline scripts. While I can also see the nonce being part of the directive in the CSP.

    Any clues about what can be the issue?

  • 🇨🇦Canada gapple

    @ricovandevin
    "some of them" is weird - my first thought is that GTM is adding the nonce as expected, but one of the scripts you're having GTM insert has its own loader that doesn't propagate the nonce to its additional inserted scripts?

    and a note regarding the nonce values not being visible: the nonce isn't exposed to CSS, and so also not shown in the browser tools page inspector, but you should be able to access it through JS in the console by using querySelectorAll and looking at the returned element objects' properties.

  • 🇨🇦Canada gapple

    Latest change uses the Policy Helper service added in 1.32 (and 2.0) instead of a custom method for adding the nonce when possible. The tests somewhat duplicate the Policy Helper's own tests, but still good to test the specific use case.

    (The policy helper service will also be backported to a 1.25 release that's compatible with D9 for any stragglers, but it's not ready yet because I need to setup another local environment to test it first...)

  • 🇳🇱Netherlands ricovandevin

    Adding the current version of the MR as a patch for ease of use.

    Thanks for the feedback @gapple. I will forward your input to the people in charge of managing the GTM container and let them check for proper propagation of the nonce.

  • First commit to issue fork.
  • 🇨🇦Canada gapple

    I'm not sure why PHPUnit tests are failing with PHP Fatal error: Trait "Drupal\Tests\csp\Unit\AssertPolicyTrait" not found in /builds/issue/google_tag-3203811/tests/src/Unit/EventSubscriber/CspSubscriberTest.php on line 19. The composer tasks are showing that it's downloading CSP 2.0.0.

  • Pipeline finished with Failed
    4 months ago
    Total: 2081s
    #238889
  • 🇨🇦Canada gapple

    🤦 solved: CSP had its /tests/ folder excluded from packaged releases, so the trait wasn't available.
    Fixed in CSP 2.0.1; the 8.x-1.x branch did not exclude the folder.

    Not sure why the Nightwatch tests are failing though.

  • Pipeline finished with Success
    4 months ago
    Total: 221s
    #239073
  • Pipeline finished with Success
    4 months ago
    Total: 217s
    #239078
  • 🇺🇸United States spfaffly

    spfaffly changed the visibility of the branch 3203811-csp-domain to active.

  • 🇫🇮Finland YevKo Espoo

    For those who are facing the same problem with failing to use the plain diff https://git.drupalcode.org/project/google_tag/-/merge_requests/58.diff ( happens for me on 2.0.6 version): the issue is with the lack of quotes around the path of the newly added file. When generated with git diff --staged --patch > xxx.patch from the command line the patch applies. And attached here.

    3203811-34.patch

  • 🇨🇦Canada b_sharpe

    The MR appears to add the domains (if connect/img-src enabled) and nonce value as expected, but I still am getting blocks on scripts added by GTM.

    (Report-Only policy) The page’s settings would block a script (script-src-elem) xxx from being executed because it violates the following directive: “script-src-elem 'self' 'report-sample' http://cdn.jsdelivr.net https://cdnjs.cloudflare.com 'nonce-EjwscYt6M7NBaMfSm3IMrw'”

  • 🇩🇪Germany berliner

    FYI that the nonce value must be unique for every single request, so caching the nonce is not an option: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Securi...

    This is in reply to #3203811-18: Support Content Security Policy

  • 🇨🇦Canada gapple

    @berliner From the one post I've seen that's worked through the impact of full-page caching with a CSP nonce https://serverfault.com/a/1064775/66144:

    Let's say you as an attacker are served the same nonce as your victim on a response to the URL /foo/bar. Now you could use this nonce in your XSS payload. Let's consider two of the most common XSS scenario's:
    - URL parameter based reflected XSS: /foo/bar?payload=... will likely have a different cache key than /foo/bar, therefore causing the user to be delivered a fresh response with a new nonce. The attack fails.
    - Stored XSS in dynamic HTML: your payload is now included when the user loads /foo/bar. However, if the user refreshes and gets a response from the cache, then this won't include your injected XSS payload. Alternatively, if they get a fresh response they will also get a fresh nonce. In either case the attack fails.

    Now, this doesn't mean that this situation can never be exploited: for example, if /foo/bar has vulnerable JavaScript that modifies the DOM in a way that the attacker payload can be injected it might possible to use the cached nonce. Think of DOM based XSS attacks via data in the # segment of a URL or in AJAX responses.

    It will be much trickier for an attacker to exploit the latter situation though, which may be sufficient since the point of CSP is to just provide defense-in-depth; it can not guarantee immunity for 100% of XSS attacks.

    For Drupal, that means to not use AJAX responses to inject unsanitized markup, from user provided data, containing an inline script with a nonce attribute. This would also only be an effective attack against anonymous users on a single page for the duration of the calling page's cache lifetime, and other pages that receive the injected inline script with a nonce (and the original page when it's re-rendered with a new nonce) will send a violation report that should prompt investigation to address the underlying markup injection.

    (AddJsCommand for adding new script files to the page through AJAX does not (currently) propagate the page's nonce to the new scripts, so they must be permitted by the originating page's policy through another source like 'self'.)

    The CSP module includes a test that authenticated users receive a unique nonce for each request of a page that is cacheable in the dynamic page cache.

    ----

    I've seen an approach where an edge cache worker handles replacing a placeholder value from the application, so that even responses served from the cache receive a unique nonce. But if the worker fails then responses are receiving a truly static value for all requests, and some examples I've seen have used a potentially predictable placeholder value such that markup injection would also be replaced with a valid nonce.
    This would be possible with the CSP module by swapping out the Nonce service to one that responds with a static nonce value that includes some secret shared between the application and edge cache.

  • 🇩🇪Germany rgpublic Düsseldorf 🇩🇪 🇪🇺

    Has providing a hash also been considered? This could be an alternative to using a nonce.

  • 🇨🇦Canada gapple

    @rgpublic A hash can't be used on the external gtm or gtag js because their contents aren't known or static. The loader file could change at any time, making the hash invalid, and any additional dynamically loaded scripts won't be permitted.
    The tag manager script loader will propagate the nonce to additional scripts (as verified by @chrissnyder in #12), without requiring the originating page to know the script contents (for a hash) or source domain (for non-tag manager scripts) to place in the policy.

Production build 0.71.5 2024