Skip to content

Commit f415168

Browse files
committed
feat(@angular/build): add CSP nonce to stylesheet and modulepreload link elements
Under strict Content Security Policies that do not allow 'self' in style-src or style-src-elem, external stylesheets must be authorized via a cryptographic nonce. This commit updates addNonce to apply the CSP nonce to <link> elements with rel containing 'stylesheet' or 'modulepreload'. Closes #34255
1 parent 564d6d8 commit f415168

3 files changed

Lines changed: 64 additions & 5 deletions

File tree

‎packages/angular/build/src/utils/index-file/inline-critical-css_spec.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,31 @@ describe('inlineCriticalCss', () => {
233233
expect(warnings.length).toBeGreaterThan(0);
234234
expect(warnings[0]).toContain('missing.css');
235235
});
236+
237+
it('should retain nonce on stylesheet links and noscript fallback', async () => {
238+
const initialContent = `
239+
<html>
240+
<head>
241+
<link href="styles.css" rel="stylesheet" nonce="test-nonce">
242+
</head>
243+
<body></body>
244+
</html>`;
245+
246+
const { content, errors, warnings } = await inlineCriticalCss(
247+
initialContent,
248+
'/dist/',
249+
undefined,
250+
false,
251+
readAsset,
252+
);
253+
254+
expect(errors).toEqual([]);
255+
expect(warnings).toEqual([]);
256+
expect(content).toContain(
257+
'<link href="styles.css" rel="stylesheet" nonce="test-nonce" media="print" data-beasties-media="all">',
258+
);
259+
expect(content).toContain(
260+
'<noscript><link href="styles.css" rel="stylesheet" nonce="test-nonce"></noscript>',
261+
);
262+
});
236263
});

‎packages/angular/build/src/utils/index-file/nonce.ts‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,26 @@ export async function addNonce(html: string): Promise<string> {
2828
const { rewriter, transformedContent } = await htmlRewritingStream(html);
2929

3030
rewriter.on('startTag', (tag) => {
31-
if (
32-
(tag.tagName === 'style' || tag.tagName === 'script') &&
33-
!tag.attrs.some((attr) => attr.name === 'nonce')
34-
) {
35-
tag.attrs.push({ name: 'nonce', value: nonce });
31+
const { tagName, attrs } = tag;
32+
33+
if (tagName === 'style' || tagName === 'script' || tagName === 'link') {
34+
let isTarget = tagName !== 'link';
35+
let hasNonce = false;
36+
37+
for (const attr of attrs) {
38+
if (attr.name === 'nonce') {
39+
hasNonce = true;
40+
} else if (tagName === 'link' && attr.name === 'rel' && attr.value) {
41+
const tokens = attr.value.trim().split(/\s+/);
42+
if (tokens.includes('stylesheet') || tokens.includes('modulepreload')) {
43+
isTarget = true;
44+
}
45+
}
46+
}
47+
48+
if (isTarget && !hasNonce) {
49+
attrs.push({ name: 'nonce', value: nonce });
50+
}
3651
}
3752

3853
rewriter.emitStartTag(tag);

‎packages/angular/build/src/utils/index-file/nonce_spec.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,21 @@ describe('addNonce', () => {
9292
expect(result).toContain('<script src="./main.js" nonce="{% nonce %}"></script>');
9393
expect(result).toContain(`<script nonce="{% nonce %}">console.log('bar');</script>`);
9494
});
95+
96+
it('should add the nonce expression to stylesheet link tags', async () => {
97+
const result = await addNonce(`
98+
<html>
99+
<head>
100+
<link rel="stylesheet" href="styles.css">
101+
<link rel="preload" href="font.woff2" as="font">
102+
</head>
103+
<body>
104+
<app ngCspNonce="{% nonce %}"></app>
105+
</body>
106+
</html>
107+
`);
108+
109+
expect(result).toContain('<link rel="stylesheet" href="styles.css" nonce="{% nonce %}">');
110+
expect(result).toContain('<link rel="preload" href="font.woff2" as="font">');
111+
});
95112
});

0 commit comments

Comments
 (0)