hyden97
hyden97

Reputation: 155

CSP google tag manager issue in node js

In my landing page(ejs) I have the snippet of google tag manager, but I have the CSP that doesn't allow inline scripts.. on the server-side(app.js) I generate on each request a nonce id.

const nonceToken = 'nonce-' + crypto.randomBytes(16).toString('base64');

the code:

app.get('/', (req, res) => {
    res.render('index', {
        nounceToken: nonceToken
    })
});

this is the snippet inside the ejs file:

<!-- Google Tag Manager -->
    <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; f.parentNode.insertBefore(j, f);
        })(window, document, 'script', 'dataLayer', 'GTM-*******');</script>
    <!-- End Google Tag Manager -->

in the helmet options :

app.use(helmet.contentSecurityPolicy({
    useDefaults: true,
    directives: {
        "script-src-elem": ["'self'", "https://unpkg.com/scrollreveal", "https://www.googletagmanager.com/gtm.js?id=GTM-*******"],
        "form-action": 'self',
        "script-src": 'nonce-' + nonceToken,
    }
}));

Errors given in the console of Google:
The source list for Content Security Policy directive 'script-src-elem' contains a source with an invalid path: '/gtm.js?id=GTM-*******'. The query component, including the '?', will be ignored.

and also:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src-elem 'self' https://unpkg.com/scrollreveal https://www.googletagmanager.com/gtm.js?id=GTM-*******". Either the 'unsafe-inline' keyword, a hash ('sha256-1KheQ9YFAlUN7PPKDGu2i9+u4UTLBUuJvOgrQJPK1lg='), or a nonce ('nonce-...') is required to enable inline execution.

i have tried a tons of times to find a solution to that but nothing..

Upvotes: 3

Views: 2376

Answers (1)

granty
granty

Reputation: 8546

  1. The const nonceToken = 'nonce-' + crypto.randomBytes(16).toString('base64'); gives you nonce-ab3d5...ef string already starting with nonce-. But in Helmet settings you add nonce- string once more: "script-src": 'nonce-' + nonceToken, therefore you'll get 'nonce-nonce-ab3d5...ef' token.

  2. The nonce-value token should be a single-quoted: 'nonce-value'.

  3. Google Tag Manager's inline script in the snippet inside the ejs file does not contains nonce='ab3d5...ef' attribute therefore it will be blocked. You have to use <script nonce='ab3d5...ef'>...</script> tag with the valid nonce attribute.

  4. In the Helmet you use the script-src-elem direcive in pair with the script-src. Only Chrome browser supports the script-src-elem, (other browsers will use the script-src).
    So Chrome will use script-src-elem": 'self'", "https://unpkg.com/scrollreveal", "https://www.googletagmanager.com/gtm.js?id=GTM-*******" which does not contain 'nonce-value' token.
    Other browsers will use "script-src": 'nonce-' + nonceToken therefore they will not allow external scripts which do not have nonce='ab3d5...ef' attribute (as <script src='...' nonce='ab3d5...ef'>).

  5. The https://unpkg.com/scrollreveal host-source do not have a trailing slash / therefore scrollreveal will be treated as file name but not folder name. Any source like https://unpkg.com/scrollreveal/dist/scrollreveal.min.js will be prohibited whereas https://unpkg.com/scrollreveal/ allows it.

  6. The host-sources like https://www.googletagmanager.com/gtm.js?id=... with a query-string component are not allowed in CSP, therefore you have to use https://www.googletagmanager.com/gtm.js or https://www.googletagmanager.com.

So finally you need to modify you code a little bit:

const nonceToken = crypto.randomBytes(16).toString('base64');  // To remove "'nonce-' +"

<!-- Google Tag Manager -->
<script nonce='${nonceToken}'>(function (w, d, s, l, i) { // To add nonce='ab3d5...ef' attribute
        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; f.parentNode.insertBefore(j, f);
    })(window, document, 'script', 'dataLayer', 'GTM-*******');</script>
<!-- End Google Tag Manager -->

app.use(helmet.contentSecurityPolicy({
    useDefaults: true,
    directives: {
        "script-src": ["'self'", `'nonce-${nonceToken}'`, "https://unpkg.com/scrollreveal/", "https://www.googletagmanager.com"],
        "form-action": 'self',
        // Other CSP directives which differs from useDefaults() here
    }
}));

If I understand correctly, the ${nonceToken} should output a real value of nonceToken. Although in the Electron is usually used construction like:

app.use((req, res, next) => {
    res.locals.cspNonce = crypto.randomBytes(16).toString("hex"); // res.locals.cspNonce will be available everywhere
    next();
    });

app.use((req, res, next) =>
    helmet.contentSecurityPolicy({
      useDefaults: true,
      directives: {
        ...
        scriptSrc: [
          `'nonce-${res.locals.nonce}'`,
          "'self'",
          "https://unpkg.com/scrollreveal/",
          "https://www.googletagmanager.com"
        ],
      },
    })(req, res, next))

Upvotes: 1

Related Questions