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.

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 -subresourceIntegrity, 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!

First, 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. We will do this 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)
    }
    add_header Content-Security-Policy "default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' www.googletagmanager.com https://www.google-analytics.com data:; font-src 'self' https://fonts.gstatic.com; connect-src *; script-src 'self' 'nonce-$cspNonce' https://www.google-analytics.com https://ssl.google-analytics.com; frame-src 'self'; report-uri /.well-known/csp-violation-report-endpoint/;";

    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;
}

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.

Share This

Share this post with your friends!