Blog / web app security / Angular Content-Security-Policy Complex Nonce: Google Tag Manager

Angular Content-Security-Policy Complex Nonce: Google Tag Manager

mozilla-observatory-security-scan

Web applications get exploited, leading to economic and reputation damage. Rich content is difficult to protect. Complex standards and complex tooling fight with each other. New technologies like Angular Single Page Applications and externally-driven analytics make it difficult to construct a valid Content-Security-Policy. If you get this wrong, you get malware injected on your site, as I wrote about here. Today let’s learn how, if we must have an inline script, we can do so with content-security-policy complex nonce

Setting up Google Analytics (via Google Tag Manager) is difficult to understand and achieve securely: you are running Javascript fetched from an inline script, how do you cause it to be trusted without destroying all trust? The answer: a nonce. Yes, content-security-policy is complex without the nonce. Set the nonce (to a unique value) on each page load, and use this to indicate what scripts your page has requested.

I’ve talked earlier about the complexities of web security, about how hard it is to balance security and functionality. One of the tools that Content-Security-Policy allows is the Nonce. The Nonce must be set differently on each HTTP response, making it complex: it requires participation of the server.

In an Angular project, we normally use -aot and -subresource-integrity, this sets a secure hash on each resource that we build and serve. However, anything that is fetched externally (e.g. Google Analytics) is more challenging. The recommended way of using Google Analytics is via Google Tag Manager. In turn, you must use a Nonce with it (as shown here). How can we set that Nonce to be unique, on each request, in an Angular SPA? Read on!

Before we start adding the nonce to external scripts, lets ensure we have hash-based subresource integrity enabled for all internal, compiled Typescript:

ng build --aot --subresource-integrity --outputHashing=all --prod=true

If we look at our index.html in the dist directory, we will now see it looks something like:

...
<link 
rel="stylesheet" 
href="styles.00fa916e08ff7ba62ff7.css" 
crossorigin="anonymous" 
integrity="sha384-+tmFFHbEXmmdzqySTeVvrRc2gEDLUMX3aMSwQxwB0spQcDT4EM2T6uHhCQ840BVW"/>
</head>
<body>
...
<script 
src="runtime.7b63b9fd40098a2e8207.js" 
crossorigin="anonymous" 
defer 
integrity="sha384-l94orWp9/B8nJYxIHArstzxjyopeYkvuFAGAhd+P3juqcmrB3UHmRx4r1Ib76hcW">
</script>
...

Those ‘integrity=’ lines have a sha384 hash of the body of the file. You can read more about Subresource Integrity. These hashes were generated by Webpack as part of the angular build, in essence, for each file, it performed:

openssl dgst -sha384 -binary XXXX.js | openssl base64 -A

and this means your browser won’t load the resulting file if it has been modified. But, that is not what we are here for today, we are here for external resources (e.g. Google Tag Manager), and a safe way to allow them to be loaded, the nonce.

Next, we enable indexTransform. We cause it to, on production builds, add a script to the index.html HEAD section. We add a magic string CSP_NONCE which we will then replace in the server side (using lua in nginx).

npm i -D @angular-builders/custom-webpack
/*
* index-html.transform.ts
* This exists to modify index.html, after build, for prod,
* to insert the google tag manager
*/
import { TargetOptions } from '@angular-builders/custom-webpack';
import { environment } from './src/environments/environment.prod';
export default (targetOptions: TargetOptions, indexHtml: string) => {
let insertTag = `<!-- No script for gtm, non-prod -->`;
if (targetOptions.configuration || 'production') {
insertTag = `<script nonce=CSP_NONCE src=https://www.googletagmanager.com/gtm.js?id=${environment.gtmTag} async></script>`;
}
const i = indexHtml.indexOf('</head>');
return `${indexHtml.slice(0, i)}
${insertTag}
${indexHtml.slice(i)}`;
};
// src/environments/environment.prod.ts
export const environment = {
...
gtmTag: 'GTM-XXXXX',
...
};
// angular.json
{
...
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"aot": true,
"outputPath": "dist",
"index": "src/index.html",
"indexTransform": "./index-html.transform.ts",
...
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",

OK, no we are generating an Angular SPA with a header which loads our Google Tag Manager, with our tag, and a Nonce just waiting to be set. Note: you don’t have to use the Webpack transform, you can just hard code in the script line with the <script nonce=CSP_NONCE prefix if you prefer.

We will now replace the string nonce=CSP_NONCE with a new, per transaction value, in our nginx.conf, adding a new location /index.html which will find all CSP_NONCE in scripts, and alter:

location /index.html {
set_by_lua_block $cspNonce  {
local base64 = require('ngx.base64')
local file = assert(io.open('/dev/urandom', 'rb'))
local bytes = file:read(32)
file:close()
return base64.encode_base64url(bytes)
}
sub_filter_once off;
sub_filter_types text/html;
sub_filter_last_modified on;
sub_filter '<script nonce=CSP_NONCE' '<script nonce="$cspNonce"';
try_files $uri $uri/ /index.html;
}

Now, when we fetch our index.html, the string is replaced on each transaction.

The net affect of this is we can safely use Google Tag Manager, since it, and its chain of dependants, could only have been fetched via our code. If we have a flaw in our application, perhaps incorrectly sanitising user-generated content, it will be unable to fetch a script (since it cannot guess our Nonce). Give it a try! No more ‘*’ and ‘unsafe-*’ for Content-Security-Policy, there’s no need.

Make sure you have a Content-Security-Policy header set (you can use nginx add_header to set it) to a strong policy.

Now, lets test. My favourite tool is the Mozilla Observatory. Enter your URL, and let it scan. It will come back with a very actionable list for you. Below is an example output showing the results after we have added content-security-policy complex nonce.

Leave a Reply

Your email address will not be published.