From ff5ebf9b1244c5a01961cd3dba6bb345392aa57c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 20 Mar 2023 15:05:08 +0100 Subject: [PATCH] feat(@angular-devkit/build-angular): add CSP support for inline styles Companion change to https://github.com/angular/angular/pull/49444. Adds an HTML processor that finds the `ngCspNonce` attribute and copies its value to any inline `style` tags in the HTML. The processor runs late in the processing pipeline in order to pick up any `style` tag that might've been added by other processors (e.g. critical CSS). --- .../utils/index-file/index-html-generator.ts | 14 +++- .../src/utils/index-file/style-nonce.ts | 62 +++++++++++++++ .../src/utils/index-file/style-nonce_spec.ts | 76 +++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 packages/angular_devkit/build_angular/src/utils/index-file/style-nonce.ts create mode 100644 packages/angular_devkit/build_angular/src/utils/index-file/style-nonce_spec.ts diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/index-html-generator.ts b/packages/angular_devkit/build_angular/src/utils/index-file/index-html-generator.ts index a49651df6127..0511ce6a9920 100644 --- a/packages/angular_devkit/build_angular/src/utils/index-file/index-html-generator.ts +++ b/packages/angular_devkit/build_angular/src/utils/index-file/index-html-generator.ts @@ -14,6 +14,7 @@ import { stripBom } from '../strip-bom'; import { CrossOriginValue, Entrypoint, FileInfo, augmentIndexHtml } from './augment-index-html'; import { InlineCriticalCssProcessor } from './inline-critical-css'; import { InlineFontsProcessor } from './inline-fonts'; +import { addStyleNonce } from './style-nonce'; type IndexHtmlGeneratorPlugin = ( html: string, @@ -59,7 +60,14 @@ export class IndexHtmlGenerator { extraPlugins.push(inlineCriticalCssPlugin(this)); } - this.plugins = [augmentIndexHtmlPlugin(this), ...extraPlugins, postTransformPlugin(this)]; + this.plugins = [ + augmentIndexHtmlPlugin(this), + ...extraPlugins, + // Runs after the `extraPlugins` to capture any nonce or + // `style` tags that might've been added by them. + addStyleNoncePlugin(), + postTransformPlugin(this), + ]; } async process(options: IndexHtmlGeneratorProcessOptions): Promise { @@ -139,6 +147,10 @@ function inlineCriticalCssPlugin(generator: IndexHtmlGenerator): IndexHtmlGenera inlineCriticalCssProcessor.process(html, { outputPath: options.outputPath }); } +function addStyleNoncePlugin(): IndexHtmlGeneratorPlugin { + return (html) => addStyleNonce(html); +} + function postTransformPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorPlugin { return async (html) => (options.postTransform ? options.postTransform(html) : html); } diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/style-nonce.ts b/packages/angular_devkit/build_angular/src/utils/index-file/style-nonce.ts new file mode 100644 index 000000000000..b7469235d560 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/index-file/style-nonce.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { htmlRewritingStream } from './html-rewriting-stream'; + +/** + * Pattern matching the name of the Angular nonce attribute. Note that this is + * case-insensitive, because HTML attribute names are case-insensitive as well. + */ +const NONCE_ATTR_PATTERN = /ngCspNonce/i; + +/** + * Finds the `ngCspNonce` value and copies it to all inline ` + + + + + + + `); + + expect(result).toContain(''); + expect(result).toContain(''); + }); + + it('should add a lowercase nonce expression to style tags', async () => { + const result = await addStyleNonce(` + + + + + + + + + `); + + expect(result).toContain(''); + }); + + it('should preserve any pre-existing nonces', async () => { + const result = await addStyleNonce(` + + + + + + + + + + `); + + expect(result).toContain(''); + expect(result).toContain(''); + }); + + it('should use the first nonce that is defined on the page', async () => { + const result = await addStyleNonce(` + + + + + + + + + + `); + + expect(result).toContain(''); + }); +});