- Merge request !58Issue #3203811: Support Content Security Policy nonce → (Open) created by chrissnyder
- last update
about 1 year ago 54 pass - 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 5:21pm 26 September 2023 - last update
about 1 year ago 54 pass - 🇨🇦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 authorizedHowever, 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 withscript-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 forscript-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 forconnect-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
- last update
about 1 year ago 54 pass - 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 whyCsp::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 toscript-src
andscript-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? - Merged domain changes into the nonce MR, so that pages with
- last update
about 1 year ago Build Successful - 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.
- 🇺🇸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. - 🇨🇦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.
- 🇺🇸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.
- 🇨🇦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
orgtag
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.