From 81795e0699c317f7120cf32ed4192557681e7a46 Mon Sep 17 00:00:00 2001 From: yoggydev <280342032+yoggydev@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:49:39 +0900 Subject: [PATCH 1/2] fix(@angular/build): encode script type in auto-CSP loader createLoaderScript() interpolates four script attributes into the generated loader. integrity and crossOrigin are encoded with JSON.stringify and \u003c, but type is inserted directly into a single-quoted JavaScript string literal. The comment above the function states that type can only be 'module', a JS MIME type or an empty string, but isJavascriptMimeType() only compares the part before the first ';', so a value such as text/javascript; reaches the loader unchanged. A quote in that value closes the string literal, and a closing script tag terminates the generated element. Encode type the same way as its neighbours. Both branches of createLoaderScript() share srcListFormatted, so one change covers Trusted Types enabled and disabled. --- .../build/src/utils/index-file/auto-csp.ts | 6 +- .../src/utils/index-file/auto-csp_spec.ts | 60 +++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/angular/build/src/utils/index-file/auto-csp.ts b/packages/angular/build/src/utils/index-file/auto-csp.ts index 5c1899015a7e..cd07fb821640 100644 --- a/packages/angular/build/src/utils/index-file/auto-csp.ts +++ b/packages/angular/build/src/utils/index-file/auto-csp.ts @@ -299,8 +299,10 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false) .map((s) => { // URI encoding means value can't escape string, JS, or HTML context. const srcAttr = encodeURI(s.src).replaceAll("'", "\\'"); - // Can only be 'module' or a JS MIME type or an empty string. - const typeAttr = s.type ? "'" + s.type + "'" : "''"; + // 'module', a JS MIME type, or an empty string. A JS MIME type may carry + // parameters after a ';', which isJavascriptMimeType() does not constrain, + // so encode this the same way as integrity and crossOrigin below. + const typeAttr = JSON.stringify(s.type ?? '').replaceAll('<', '\\u003c'); const asyncAttr = !!s.async; const deferAttr = !!s.defer; const integrityAttr = JSON.stringify(s.integrity ?? null).replaceAll('<', '\\u003c'); diff --git a/packages/angular/build/src/utils/index-file/auto-csp_spec.ts b/packages/angular/build/src/utils/index-file/auto-csp_spec.ts index 4fe2888efa42..037ab605253a 100644 --- a/packages/angular/build/src/utils/index-file/auto-csp_spec.ts +++ b/packages/angular/build/src/utils/index-file/auto-csp_spec.ts @@ -58,7 +58,7 @@ describe('auto-csp', () => { const csps = getCsps(result); expect(csps).toHaveSize(1); expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); - expect(result).toContain(`const scripts = [['./main.js', '', false, false, null, null]];`); + expect(result).toContain(`const scripts = [['./main.js', "", false, false, null, null]];`); }); it('should rewrite a single source script in place', async () => { @@ -78,7 +78,7 @@ describe('auto-csp', () => { expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); // Our loader script appears after the HTML text content. expect(result).toMatch( - /Some text<\/div>\s*`); @@ -166,12 +166,12 @@ describe('auto-csp', () => { // Loader script for main.js and main2.js appear after 'foo' and before 'bar'. expect(result).toMatch( // eslint-disable-next-line max-len - /console.log\('foo'\);<\/script>\s* + + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + // The type stays inside its string literal. + expect(result).toContain( + `const scripts = [['./main.js', "text/javascript;']];var x=1;var junk=[['a','b", false, false, null, null]];`, + ); + }); + + it('should encode a script type that contains a closing script tag', async () => { + const result = await autoCsp(` + + + + + + + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + // Only the loader element is emitted. + expect(Array.from(result.matchAll(/