Skip to content

Commit e6deb82

Browse files
alan-agius4dgp1130
authored andcommitted
fix(@angular/build): update critical CSS inlining to support autoCsp
This update improves the handling of inlined critical CSS to align with `autoCsp`, ensuring compliance with Content Security Policy (CSP) directives. Previously, inlined styles could trigger CSP violations in certain configurations. With this fix, critical CSS is inlined in a way that maintains security while supporting `autoCsp`. Closes #29603
1 parent 33ed6e8 commit e6deb82

File tree

9 files changed

+64
-29
lines changed

9 files changed

+64
-29
lines changed

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

-5
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,6 @@ export async function executeBuild(
240240
);
241241
}
242242

243-
// Override auto-CSP settings if we are serving through Vite middleware.
244-
if (context.builder.builderName === 'dev-server' && options.security) {
245-
options.security.autoCsp = false;
246-
}
247-
248243
// Perform i18n translation inlining if enabled
249244
if (i18nOptions.shouldInline) {
250245
const result = await inlineI18n(metafile, options, executionResult, initialFiles);

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

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

389+
const autoCsp = options.security?.autoCsp;
390+
const security = {
391+
autoCsp: autoCsp
392+
? {
393+
unsafeEval: autoCsp === true ? false : !!autoCsp.unsafeEval,
394+
}
395+
: undefined,
396+
};
397+
389398
// Initial options to keep
390399
const {
391400
allowedCommonJsDependencies,
@@ -415,7 +424,6 @@ export async function normalizeOptions(
415424
partialSSRBuild = false,
416425
externalRuntimeStyles,
417426
instrumentForCoverage,
418-
security,
419427
} = options;
420428

421429
// Return all the normalized options

packages/angular/build/src/builders/dev-server/vite-server.ts

+5
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ export async function* serveWithVite(
112112
browserOptions.ssr ||= true;
113113
}
114114

115+
// Disable auto CSP.
116+
browserOptions.security = {
117+
autoCsp: false,
118+
};
119+
115120
// Set all packages as external to support Vite's prebundle caching
116121
browserOptions.externalPackages = serverOptions.prebundle;
117122

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export async function autoCsp(html: string, unsafeEval = false): Promise<string>
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);

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

+20
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ describe('InlineCriticalCssProcessor', () => {
106106
expect(content).toContain('<style>body{margin:0}html{color:white}</style>');
107107
});
108108

109+
it(`should process the inline 'onload' handlers if a 'autoCsp' is true`, async () => {
110+
const inlineCssProcessor = new InlineCriticalCssProcessor({
111+
readAsset,
112+
autoCsp: true,
113+
});
114+
115+
const { content } = await inlineCssProcessor.process(getContent(''), {
116+
outputPath: '/dist/',
117+
});
118+
119+
expect(content).toContain(
120+
'<link href="styles.css" rel="stylesheet" media="print" ngCspMedia="all">',
121+
);
122+
expect(tags.stripIndents`${content}`).toContain(tags.stripIndents`
123+
<style>
124+
body { margin: 0; }
125+
html { color: white; }
126+
</style>`);
127+
});
128+
109129
it('should process the inline `onload` handlers if a CSP nonce is specified', async () => {
110130
const inlineCssProcessor = new InlineCriticalCssProcessor({
111131
readAsset,

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)