Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ff5ebf9

Browse files
crisbetoangular-robot[bot]
authored andcommittedMar 20, 2023
feat(@angular-devkit/build-angular): add CSP support for inline styles
Companion change to angular/angular#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).
1 parent 5a171dd commit ff5ebf9

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed
 

‎packages/angular_devkit/build_angular/src/utils/index-file/index-html-generator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { stripBom } from '../strip-bom';
1414
import { CrossOriginValue, Entrypoint, FileInfo, augmentIndexHtml } from './augment-index-html';
1515
import { InlineCriticalCssProcessor } from './inline-critical-css';
1616
import { InlineFontsProcessor } from './inline-fonts';
17+
import { addStyleNonce } from './style-nonce';
1718

1819
type IndexHtmlGeneratorPlugin = (
1920
html: string,
@@ -59,7 +60,14 @@ export class IndexHtmlGenerator {
5960
extraPlugins.push(inlineCriticalCssPlugin(this));
6061
}
6162

62-
this.plugins = [augmentIndexHtmlPlugin(this), ...extraPlugins, postTransformPlugin(this)];
63+
this.plugins = [
64+
augmentIndexHtmlPlugin(this),
65+
...extraPlugins,
66+
// Runs after the `extraPlugins` to capture any nonce or
67+
// `style` tags that might've been added by them.
68+
addStyleNoncePlugin(),
69+
postTransformPlugin(this),
70+
];
6371
}
6472

6573
async process(options: IndexHtmlGeneratorProcessOptions): Promise<IndexHtmlTransformResult> {
@@ -139,6 +147,10 @@ function inlineCriticalCssPlugin(generator: IndexHtmlGenerator): IndexHtmlGenera
139147
inlineCriticalCssProcessor.process(html, { outputPath: options.outputPath });
140148
}
141149

150+
function addStyleNoncePlugin(): IndexHtmlGeneratorPlugin {
151+
return (html) => addStyleNonce(html);
152+
}
153+
142154
function postTransformPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorPlugin {
143155
return async (html) => (options.postTransform ? options.postTransform(html) : html);
144156
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { htmlRewritingStream } from './html-rewriting-stream';
10+
11+
/**
12+
* Pattern matching the name of the Angular nonce attribute. Note that this is
13+
* case-insensitive, because HTML attribute names are case-insensitive as well.
14+
*/
15+
const NONCE_ATTR_PATTERN = /ngCspNonce/i;
16+
17+
/**
18+
* Finds the `ngCspNonce` value and copies it to all inline `<style>` tags.
19+
* @param html Markup that should be processed.
20+
*/
21+
export async function addStyleNonce(html: string): Promise<string> {
22+
const nonce = await findNonce(html);
23+
24+
if (!nonce) {
25+
return html;
26+
}
27+
28+
const { rewriter, transformedContent } = await htmlRewritingStream(html);
29+
30+
rewriter.on('startTag', (tag) => {
31+
if (tag.tagName === 'style' && !tag.attrs.some((attr) => attr.name === 'nonce')) {
32+
tag.attrs.push({ name: 'nonce', value: nonce });
33+
}
34+
35+
rewriter.emitStartTag(tag);
36+
});
37+
38+
return transformedContent();
39+
}
40+
41+
/** Finds the Angular nonce in an HTML string. */
42+
async function findNonce(html: string): Promise<string | null> {
43+
// Inexpensive check to avoid parsing the HTML when we're sure there's no nonce.
44+
if (!NONCE_ATTR_PATTERN.test(html)) {
45+
return null;
46+
}
47+
48+
const { rewriter, transformedContent } = await htmlRewritingStream(html);
49+
let nonce: string | null = null;
50+
51+
rewriter.on('startTag', (tag) => {
52+
const nonceAttr = tag.attrs.find((attr) => NONCE_ATTR_PATTERN.test(attr.name));
53+
if (nonceAttr?.value) {
54+
nonce = nonceAttr.value;
55+
rewriter.stop(); // Stop parsing since we've found the nonce.
56+
}
57+
});
58+
59+
await transformedContent();
60+
61+
return nonce;
62+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { addStyleNonce } from './style-nonce';
10+
11+
describe('add-style-nonce', () => {
12+
it('should add the nonce expression to all inline style tags', async () => {
13+
const result = await addStyleNonce(`
14+
<html>
15+
<head>
16+
<style>.a {color: red;}</style>
17+
<style>.b {color: blue;}</style>
18+
</head>
19+
<body>
20+
<app ngCspNonce="{% nonce %}"></app>
21+
</body>
22+
</html>
23+
`);
24+
25+
expect(result).toContain('<style nonce="{% nonce %}">.a {color: red;}</style>');
26+
expect(result).toContain('<style nonce="{% nonce %}">.b {color: blue;}</style>');
27+
});
28+
29+
it('should add a lowercase nonce expression to style tags', async () => {
30+
const result = await addStyleNonce(`
31+
<html>
32+
<head>
33+
<style>.a {color: red;}</style>
34+
</head>
35+
<body>
36+
<app ngcspnonce="{% nonce %}"></app>
37+
</body>
38+
</html>
39+
`);
40+
41+
expect(result).toContain('<style nonce="{% nonce %}">.a {color: red;}</style>');
42+
});
43+
44+
it('should preserve any pre-existing nonces', async () => {
45+
const result = await addStyleNonce(`
46+
<html>
47+
<head>
48+
<style>.a {color: red;}</style>
49+
<style nonce="{% otherNonce %}">.b {color: blue;}</style>
50+
</head>
51+
<body>
52+
<app ngCspNonce="{% nonce %}"></app>
53+
</body>
54+
</html>
55+
`);
56+
57+
expect(result).toContain('<style nonce="{% nonce %}">.a {color: red;}</style>');
58+
expect(result).toContain('<style nonce="{% otherNonce %}">.b {color: blue;}</style>');
59+
});
60+
61+
it('should use the first nonce that is defined on the page', async () => {
62+
const result = await addStyleNonce(`
63+
<html>
64+
<head>
65+
<style>.a {color: red;}</style>
66+
</head>
67+
<body>
68+
<app ngCspNonce="{% nonce %}"></app>
69+
<other-app ngCspNonce="{% otherNonce %}"></other-app>
70+
</body>
71+
</html>
72+
`);
73+
74+
expect(result).toContain('<style nonce="{% nonce %}">.a {color: red;}</style>');
75+
});
76+
});

0 commit comments

Comments
 (0)
Please sign in to comment.