Reputation: 155
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
Reputation: 8546
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.
The nonce-value
token should be a single-quoted: 'nonce-value'
.
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.
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'>
).
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.
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