Skip to content

Commit e8b3b5b

Browse files
committed
fix(@angular/build): update autoCsp to handle inlined critical CSS
This update enhances the `autoCsp` functionality to properly handle inlined critical CSS, ensuring compliance with Content Security Policy (CSP) directives. Previously, inlined styles could cause CSP violations in certain configurations. With this fix, the mechanism correctly accounts for and integrates critical CSS while maintaining security. Closes angular#29603
1 parent 57a08c9 commit e8b3b5b

File tree

7 files changed

+42
-26
lines changed

7 files changed

+42
-26
lines changed

packages/angular/build/src/builders/application/execute-build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export async function executeBuild(
242242

243243
// Override auto-CSP settings if we are serving through Vite middleware.
244244
if (context.builder.builderName === 'dev-server' && options.security) {
245-
options.security.autoCsp = false;
245+
options.security = undefined;
246246
}
247247

248248
// Perform i18n translation inlining if enabled

packages/angular/build/src/builders/application/options.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ export async function normalizeOptions(
386386
}
387387
}
388388

389+
const autoCsp = options.security?.autoCsp;
390+
let security;
391+
if (autoCsp) {
392+
security = {
393+
autoCsp: {
394+
unsafeEval: autoCsp === true ? false : !!autoCsp?.unsafeEval,
395+
},
396+
};
397+
}
398+
389399
// Initial options to keep
390400
const {
391401
allowedCommonJsDependencies,
@@ -415,7 +425,6 @@ export async function normalizeOptions(
415425
partialSSRBuild = false,
416426
externalRuntimeStyles,
417427
instrumentForCoverage,
418-
security,
419428
} = options;
420429

421430
// Return all the normalized options

packages/angular/build/src/tools/esbuild/index-html-generator.ts

+1-10
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,6 @@ export async function generateIndexHtml(
8080
throw new Error(`Output file does not exist: ${relativefilePath}`);
8181
};
8282

83-
// Read the Auto CSP options.
84-
const autoCsp = buildOptions.security?.autoCsp;
85-
const autoCspOptions =
86-
autoCsp === true
87-
? { unsafeEval: false }
88-
: autoCsp
89-
? { unsafeEval: !!autoCsp.unsafeEval }
90-
: undefined;
91-
9283
// Create an index HTML generator that reads from the in-memory output files
9384
const indexHtmlGenerator = new IndexHtmlGenerator({
9485
indexPath: indexHtmlOptions.input,
@@ -103,7 +94,7 @@ export async function generateIndexHtml(
10394
buildOptions.prerenderOptions ||
10495
buildOptions.appShellOptions
10596
),
106-
autoCsp: autoCspOptions,
97+
autoCsp: buildOptions.security?.autoCsp,
10798
});
10899

109100
indexHtmlGenerator.readAsset = readAsset;

packages/angular/build/src/utils/index-file/auto-csp.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ export async function autoCsp(html: string, unsafeEval = false): Promise<string>
9494
function emitLoaderScript() {
9595
const loaderScript = createLoaderScript(scriptContent, /* enableTrustedTypes = */ false);
9696
hashes.push(hashTextContent(loaderScript));
97-
rewriter.emitRaw(`<script>${loaderScript}</script>`);
97+
rewriter.emitRaw(`<script type="text/javascript">${loaderScript}</script>`);
9898
scriptContent = [];
9999
}
100100

101-
rewriter.on('startTag', (tag, html) => {
101+
rewriter.on('startTag', (tag) => {
102102
if (tag.tagName === 'script') {
103103
openedScriptTag = tag;
104104
const src = getScriptAttributeValue(tag, 'src');

packages/angular/build/src/utils/index-file/index-html-generator.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class IndexHtmlGenerator {
8282

8383
// CSR plugins
8484
if (options?.optimization?.styles?.inlineCritical) {
85-
this.csrPlugins.push(inlineCriticalCssPlugin(this));
85+
this.csrPlugins.push(inlineCriticalCssPlugin(this, !!options.autoCsp));
8686
}
8787

8888
this.csrPlugins.push(addNoncePlugin());
@@ -197,11 +197,15 @@ function inlineFontsPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorP
197197
return async (html) => inlineFontsProcessor.process(html);
198198
}
199199

200-
function inlineCriticalCssPlugin(generator: IndexHtmlGenerator): IndexHtmlGeneratorPlugin {
200+
function inlineCriticalCssPlugin(
201+
generator: IndexHtmlGenerator,
202+
autoCsp: boolean,
203+
): IndexHtmlGeneratorPlugin {
201204
const inlineCriticalCssProcessor = new InlineCriticalCssProcessor({
202205
minify: generator.options.optimization?.styles.minify,
203206
deployUrl: generator.options.deployUrl,
204207
readAsset: (filePath) => generator.readAsset(filePath),
208+
autoCsp,
205209
});
206210

207211
return async (html, options) =>

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

+14-8
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface InlineCriticalCssProcessorOptions {
6868
minify?: boolean;
6969
deployUrl?: string;
7070
readAsset?: (path: string) => Promise<string>;
71+
autoCsp?: boolean;
7172
}
7273

7374
/** Partial representation of an `HTMLElement`. */
@@ -163,7 +164,7 @@ class BeastiesExtended extends BeastiesBase {
163164
const returnValue = await super.embedLinkedStylesheet(link, document);
164165
const cspNonce = this.findCspNonce(document);
165166

166-
if (cspNonce) {
167+
if (cspNonce || this.optionsExtended.autoCsp) {
167168
const beastiesMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
168169

169170
if (beastiesMedia) {
@@ -180,11 +181,13 @@ class BeastiesExtended extends BeastiesBase {
180181
// a way of doing that at the moment so we fall back to doing it any time a `link` tag is
181182
// inserted. We mitigate it by only iterating the direct children of the `<head>` which
182183
// should be pretty shallow.
183-
document.head.children.forEach((child) => {
184-
if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
185-
child.setAttribute('nonce', cspNonce);
186-
}
187-
});
184+
if (cspNonce) {
185+
document.head.children.forEach((child) => {
186+
if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
187+
child.setAttribute('nonce', cspNonce);
188+
}
189+
});
190+
}
188191
}
189192

190193
return returnValue;
@@ -215,16 +218,19 @@ class BeastiesExtended extends BeastiesBase {
215218
*/
216219
private conditionallyInsertCspLoadingScript(
217220
document: PartialDocument,
218-
nonce: string,
221+
nonce: string | null,
219222
link: PartialHTMLElement,
220223
): void {
221224
if (this.addedCspScriptsDocuments.has(document)) {
222225
return;
223226
}
224227

225228
const script = document.createElement('script');
226-
script.setAttribute('nonce', nonce);
227229
script.textContent = LINK_LOAD_SCRIPT_CONTENT;
230+
if (nonce) {
231+
script.setAttribute('nonce', nonce);
232+
}
233+
228234
// Prepend the script to the head since it needs to
229235
// run as early as possible, before the `link` tags.
230236
document.head.insertBefore(script, link);

tests/legacy-cli/e2e/tests/build/auto-csp.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert';
22
import { getGlobalVariable } from '../../utils/env';
3-
import { expectFileToMatch, writeMultipleFiles } from '../../utils/fs';
3+
import { expectFileToMatch, writeFile, writeMultipleFiles } from '../../utils/fs';
44
import { findFreePort } from '../../utils/network';
55
import { execAndWaitForOutputToMatch, ng } from '../../utils/process';
66
import { updateJsonFile } from '../../utils/project';
@@ -13,6 +13,9 @@ export default async function () {
1313
'This test should not be called in the Webpack suite.',
1414
);
1515

16+
// Add global css to trigger critical css inlining
17+
await writeFile('src/styles.css', `body { color: green }`);
18+
1619
// Turn on auto-CSP
1720
await updateJsonFile('angular.json', (json) => {
1821
const build = json['projects']['test-project']['architect']['build'];
@@ -54,7 +57,7 @@ export default async function () {
5457
</head>
5558
<body>
5659
<app-root></app-root>
57-
60+
5861
<script>
5962
const inlineScriptBodyCreated = 1338;
6063
console.warn("Inline Script Body: " + inlineScriptHeadCreated);
@@ -130,6 +133,9 @@ export default async function () {
130133
// Make sure the output files have auto-CSP as a result of `ng build`
131134
await expectFileToMatch('dist/test-project/browser/index.html', CSP_META_TAG);
132135

136+
// Make sure if contains the critical CSS inlining CSP code.
137+
await expectFileToMatch('dist/test-project/browser/index.html', 'ngCspMedia');
138+
133139
// Make sure that our e2e protractor tests run to confirm that our angular project runs.
134140
const port = await spawnServer();
135141
await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target=');

0 commit comments

Comments
 (0)