Questions about adding nonces

Created on 28 January 2025, 2 months ago

Is it possible to with this module to add a nonce to a script tag generated by e.g. this module
https://git.drupalcode.org/project/datalayer/-/blob/2.0.x/src/DatalayerL...

💬 Support request
Status

Active

Version

2.0

Component

Code

Created by

🇳🇱Netherlands undersound3

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

Comments & Activities

  • Issue created by @undersound3
  • 🇨🇦Canada gapple

    I don't think you would want to add the nonce to the dataLayer, but you can access the nonce server-side through the csp.nonce service, or in scripts with drupalSettings.csp.nonce by adding the csp.nonce library as a dependency.

  • 🇳🇱Netherlands undersound3
  • 🇳🇱Netherlands undersound3

    @gapple thanks for your response.

    Could you elaborate as to why I probably wouldn't want to add a nonce to this script?
    Is this not necessary because it is not a security thread or are their other reasons?

  • 🇨🇦Canada gapple

    I think I misunderstood - I thought you had meant adding the nonce as a property to the JS dataLayer object.

    The module's documentation has an example of how to add a nonce to a render element → . In this case since datalayer is already rendering in a lazybuilder, it doesn't have to (shouldn't?) use CSP's nonce lazybuilder.
    I'm pretty sure #attached gets bubbled up from lazy-built elements for CSP to have access to in it's response processor, but that would have to be double-checked.

    if (\Drupal::service('module_handler')->moduleExists('csp')) {
      $element['#attributes']['nonce'] = \Drupal::service('csp.nonce')->getValue();
      $element['#attached']['csp_nonce'] = [
        'script' => [Csp::POLICY_UNSAFE_INLINE],
      ];
    }
    

    This requires CSP >= 2.1.0, so the module should also add a conflict to its composer.json to ensure it's not installed with an older version that would cause core to through an exception when encountering ['#attached']['csp_nonce'] :

    "conflict": {
      "drupal/csp": "<2.1.0-beta1"
    }
    
  • 🇳🇱Netherlands undersound3

    You are correct, I meant adding the nonce to the inline script tag generated by this function https://git.drupalcode.org/project/datalayer/-/blob/2.0.x/src/DatalayerL...

    So in the header I want
    Content-Security-Policy: script-src 'nonce-2726c7f26c'

    And I want the inline script tag from the datalayer module to look like:
    <script nonce="2726c7f26c">window.dataLayer = window.dataLayer || []; window.dataLayer.push({"drupalLanguage"....</script>

    What I am unsure about is where to apply the code example you provided.
    Let's say I don't want to patch the datalayer module for now but want use a hook in my custom module to add the nonce.
    What would be the hook to use? hook_element_info_alter?
    Do I need to create a subscriber to add the nonce to the header or will the CSP module add this?

    Thanks in advance for any guidance..

  • 🇨🇦Canada gapple

    You could decorate datalayer's lazy builder class to alter the element when it's being built.

    mymodule.services.yml

      datalayer.lazy_builders.mymodule:
        public: false
        class: Drupal\mymodule\DatalayerBuilder
        decorates: datalayer.lazy_builders
        arguments: ['@datalayer.lazy_builders.inner', '@csp.nonce_builder']
    

    mymodule/src/DatalayerBuilder.php

    namespace Drupal\mymodule;
    
    use Drupal\csp\Csp;
    use Drupal\csp\NonceBuilder;
    use Drupal\datalayer\DatalayerLazyBuilders;
    use Drupal\Core\Security\Attribute\TrustedCallback;
    
    class DatalayerBuilder {
    
      public function __construct(
        protected DatalayerLazyBuilders $decorated,
        protected NonceBuilder $nonce_builder
      ) {
      
      }
    
      #[TrustedCallback]
      public function build() {
        $build = $decorated->lazyScriptTag();
    
        $placeholderKey = $nonce_builder->getPlaceholderKey();
    
        $build['datalayer']['#attributes']['nonce'] = $placeholderKey;
        $build['datalayer']['#attached']['csp_nonce'] = [
          'script' => [Csp::POLICY_UNSAFE_INLINE],
        ];
        $build['datalayer']['#attached']['placeholders'][$placeholderKey] = [
          '#lazy_builder' => ['csp.nonce_builder:renderNonce', []],
        ];
    
        return $build;
      }
    }
    
    

    ----

    Setting $build['datalayer']['#attached']['csp_nonce'] tells CSP module that it needs to add the nonce generated for each request to the header if possible, and the placeholder in the script's attribute will be replaced with the same value.

  • 🇳🇱Netherlands undersound3

    Thanks for providing this suggestion. When using this example I am getting the following error:

    The service "datalayer.lazy_builders" has a dependency on a non-existent service "datalayer.lazy_builders.inner".

  • 🇳🇱Netherlands undersound3

    Trying a bit further....

    In the services file I changed the first argument to the id of the service from the custom module instead of the service id from the datalayer module.
    So @datalayer.lazy_builders.inner became @datalayer.lazy_builders.my_module.inner

    Now when executing I get the following message:

    LogicException: You are not allowed to use csp_nonce in #attached. in Drupal\Core\Render\HtmlResponseAttachmentsProcessor->processAttachments() (line 152 of core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php).

    My files

    my_module/my_module.services.yml

    services:
      datalayer.lazy_builders.my_module:
        public: false
        class: Drupal\my_module\DatalayerBuilder
        decorates: datalayer.lazy_builders
        arguments: ['@datalayer.lazy_builders.my_module.inner', '@csp.nonce_builder']
    

    my_module/src/DatalayerBuilder.php

    <?php
    
    namespace Drupal\my_module;
    
    use Drupal\csp\Csp;
    use Drupal\csp\NonceBuilder;
    use Drupal\datalayer\DatalayerLazyBuilders;
    use Drupal\Core\Security\Attribute\TrustedCallback;
    
    class DatalayerBuilder {
    
      /**
       * @var \Drupal\datalayer\DatalayerLazyBuilders
       */
      private DatalayerLazyBuilders $datalayerLazyBuilders;
    
      /**
       * @var \Drupal\csp\NonceBuilder
       */
      private NonceBuilder $nonceBuilder;
    
      public function __construct(
        protected DatalayerLazyBuilders $decorated,
        protected NonceBuilder $nonce_builder
      ) {
        $this->datalayerLazyBuilders = $decorated;
        $this->nonceBuilder = $nonce_builder;
      }
    
      #[TrustedCallback]
      public function lazyScriptTag() {
        $build = $this->datalayerLazyBuilders->lazyScriptTag();
    
        $placeholderKey =  $this->nonceBuilder->getPlaceholderKey();
    
        $build['datalayer']['#attributes']['nonce'] = $placeholderKey;
        $build['datalayer']['#attached']['csp_nonce'] = [
          'script' => [Csp::POLICY_UNSAFE_INLINE],
        ];
        $build['datalayer']['#attached']['placeholders'][$placeholderKey] = [
          '#lazy_builder' => ['csp.nonce_builder:renderNonce', []],
        ];
    
        return $build;
      }
    }
    
  • 🇨🇦Canada gapple

    I wrote the code in the comment box without running it so there may be typos or bugs 😜.

    🤔 decorating would currently require that the datalayer lazy builder be executed before \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::processAttachments(), so that CSP's decorator of that class can hide it's own '#attached' properties that core doesn't like, but it looks like that method is actually executing render element lazy builders itself.
    It may require ✨ Allow additional keys in #attached Active in core to work, so that core ignores the ['#attached']['csp_nonce']

    In that case, it's probably necessary to either:
    a) patch core with the above change

    b)
    Use another method than $build['datalayer']['#attached']['csp_nonce'] to add the nonce to the header.
    If you know datalayer is on every page, you could have your own subscriber to the CspEvents::POLICY_ALTER event always call \Drupal::service('csp.policy_helper')->appendNonce($policy, 'script', [Csp::POLICY_UNSAFE_INLINE]);

    or c)
    use hook_page_bottom(), weighted to ensure it's executed after datalayer_page_bottom(), to add the attribute and #attached values so they exist before the final page rendering flow is started.

  • 🇳🇱Netherlands undersound3

    I wrote the code in the comment box without running it so there may be typos or bugs 😜.

    I was not expecting you to do so and was assuming that 😜 Very thankful for it anyways, it helps a lot trying to understand the workings of this.

    Thanks for these suggestions. Tested option A) quickly by uncommenting the code in https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co... and that works indeed. I am using 10.4.1 btw.

    Will ponder upon the other suggestion and see what best fits.

    Thanks again!

  • Status changed to Fixed 10 days ago
  • 🇨🇦Canada gapple
Production build 0.71.5 2024