• Home
  • Writing
  • Resume
  • Home
  • Writing
  • Resume

Writing

  • Why Some Managers Look Disingenuous

    January 3rd, 2024
  • Do’s and Don’ts For Technical Interviews

    December 27th, 2023
  • The Book That Made Me Want To Be A Leader

    October 30th, 2023
  • How to Fail An Interview As the Interviewer

    October 25th, 2023
  • Should Managers Be Prescriptive?

    October 16th, 2023
  • How To Help Unmotivated Developers

    July 12th, 2023
  • What to do when nothing seems good enough

    July 10th, 2023
  • Why Do We Burn Out?

    July 7th, 2023
  • What I Taught You, I Don’t Know

    June 21st, 2023
  • How To Delegate Effectively Without Feeling You Are Losing Control

    June 12th, 2023
  • Let’s Accept It. Technical Interviews Are Broken

    June 7th, 2023
  • Why Managers Need Empathy to Manage Low Performers

    May 31st, 2023
  • The Slow Decline of Highly Motivated Developers

    May 24th, 2023
  • Why Writing Explicit Code Matters

    May 18th, 2023
  • Why Is It So Difficult to Assess Expertise in an Interview?

    May 15th, 2023
  • The Real Value of a Senior Developer When it Comes to Dealing With Uncertainty

    May 11th, 2023
  • Why You Should Use Feature Flags to Deploy with Confidence

    April 28th, 2023
  • Over-Engineering Is Not (Just) a Technical Problem

    March 20th, 2023
  • The proactivity fallacy

    January 25th, 2023
  • Extending typescript intersection with optional properties

    January 18th, 2023
  • Setting up Google Tag Manager in a Nextjs application with a strict content security policy

    December 27th, 2022
  • How to build a scalable folder structure for a nextjs app

    December 11th, 2022
  • Why I have stopped writing comments

    December 6th, 2022
  • How to efficiently type nextjs page's props

    December 6th, 2022

Setting up Google Tag Manager in a Nextjs application with a strict content security policy

December 27th, 2022

In this article we will cover:

  1. What is the problem between having a strict CSP and using Google Tag Manager (GTM)
  2. What is a nonce and how to generate it?
  3. How to use a nonce to allow an inline script to be executed
  4. How to pass down the nonce to GTM so we can inject inline scripts at runtime and executed without bypassing the CSP

Introduction

Google Tag Manager (GTM) is a tag management system that allows you to quickly and easily update measurement codes and related code fragments collectively known as tags on your website or mobile app. In simpler words, it allows you to execute different code snippets in your website without making changes to the source code. This is great when you work on a company that has a marketing department that wants to try different pixel tracking or retargeting tools or measure different user actions, because once set up in the website, GTM allows them to do that directly from GTM's UI without a need of a code change or deployment.

The problem with GTM is that every fragment of code that you specify to execute in the website will be inlined and if you have a Content Security Policy (CSP) that is somewhat strict (and i hope you do) all inline scripts will be blocked.

Enter nonce

The CSP specification defines a way to indicate that an inline script is to be trusted, which is by a way o using a nonce or a single use token. If the inline script contains a nonce attribute matching the nonce specified in the CSP header the script can be executed safely.

It's important to mention that there are other ways in the CSP specification to indicate that an inline is to be trusted, the most common use is via a hash where the inline script can be executed if its hash matches the specified hash in the header. The problem with this approach is that you need to know the inline scripts at build time so you cna hash them and that won't work with scripts loaded via GTM.

Generating the nonce

The nonce should be a secure random string, and should not be reused, and in the context of Nextjs, it must be generated either during SSR (server-side rendering) or during SSG (static site generation) so the browser is able to parse the CSP content with the nonce in it. In Nextjs, the place to execute code on a page during the server phase and not the client is _document.js

import { randomUUID } from 'crypto'
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
    const nonce = randomUUID()
    return (
        <Html lang="en">
            <Head />
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    )
}

Loading GTM

To load the GTM library we can just use the snippet GTM provides with two very important changes.

Before we talk about those changes, it's important to understand how the snippet provided by GTM works. GTM provides an inline script that when executed it injects another inline script that will load the GTM's library and execute it and, important to the topic at hand, both inline scripts have to be allowed by the CSP.

Now that we know that we need to accomplish, these are the changes we need to do to the loading snippet:

  1. we need to add a nonce attribute with the value that was just generated, so the script to load GTM's library can be executed
  2. we need to pass down the nonce value to the inline script created by the first inline script (this is normally referred to as being nonce-aware)
import { randomUUID } from 'crypto'
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
    const nonce = randomUUID()
    return (
        <Html lang="en">
            <Head>
                <>
                    <script
                        nonce={nonce} // Change #1
                        id="gtmScript"
                        dangerouslySetInnerHTML={{
                            __html: `<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;

                            /* Change #2 */
                            var n=d.querySelector('[nonce]'); n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));
                            
                            f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','YOUR_GTM_ID');</script>`,
                        }}
                    />
                </>
            </Head>
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    )
}

Loading scripts via GTM

Now that we loaded GTM's library securely, we need to find a way to make GTM aware of the nonce value so it can be passed down to any script loaded via GTM. At this point we leave the Nextjs world and enter GTM's (i'll focus on the setup needed but i won't go deep into the GTM specifics as there's plenty of documentation and i am really no expert on it).

First of all, we need to create a variable that has the ability to get the nonce value that was generated when the page was rendered. We can achieve that by creating a DOM element variable type and specifying from which DOM element it needs to pull the value from (any script tag with the nonce attribute will do)

Now, when you want to include a script via GTM, you just need to create a Custom HTML type tag with any code you want (loading Facebook Pixel tracking library for example) and you need to add a nonce attribute to it

<script nonce={{nonce}}>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
var a=b.querySelector('[nonce]');a&&t.setAttribute('nonce',a.nonce||a.getAttribute('nonce'));
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_FB_ID');
fbq('track', 'PageView');
</script>

Two important things to note:

  1. the double curley braces syntax in the nonce attribute is the way to interpolate a GTM variable's value. That means that when this code is injected, the nonce attribute will actually contain the value that was generated when the page was rendered
  2. you may need to make the script you're injecting nonce-aware the same way we did before with the GTM script if, for example, the script you're injecting will actually inject a second script. In the previous example, you can see this line is doing exactly that
var a=b.querySelector('[nonce]');a&&t.setAttribute('nonce',a.nonce||a.getAttribute('nonce'));

Drawbacks

I really like this approach, as it doesn't require to know all the scripts during build time, giving you the flexibility of loading scripts during runtime. It is important though to be aware that this has its own negative side, as it's allowing anyone with access to GTM panel to execute code in the website. Be aware of the risk and decide whether or not it's worth it.

Closing words

We've covered what is a nonce and why is important, how to create it and how to use it to allow an inline script to be executed. Furthermore, we've covered how to use GTM to inject inline scripts during runtime and executed them securely, without bypassing the CSP.

Months ago, when i had to deal with this situation, i struggle to find a simple way to make GTM work properly without compromising security. I hope i did a good job explaining the whole process that you have find this article useful. If you'd like me to explain further any of the decisions i made here or any other topic, feel free to contact me!

Full snippet

Here's a full snippet with everything that GTM needs in order to be fully integrated and ready to use

import { randomUUID } from 'crypto'
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
    const nonce = randomUUID()
    return (
        <Html lang="en">
            <Head>
                <>
                    <script
                        nonce={nonce}
                        id="GTMDataLayerSetup"
                        dangerouslySetInnerHTML={{
                            __html: `window.dataLayer = window.dataLayer || [];`,
                        }}
                    />
                    <script
                        nonce={nonce}
                        id="gtmScript"
                        dangerouslySetInnerHTML={{
                            __html: `<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                            new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
                            j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                            'https://www.googletagmanager.com/gtm.js?id='+i+dl;var n=d.querySelector('[nonce]');
                            n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));f.parentNode.insertBefore(j,f);
                            })(window,document,'script','dataLayer','YOUR_GTM_ID');</script>`,
                        }}
                    />
                </>
            </Head>
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    )
}