diff --git a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts index e846053e64c4..790bdb0f2969 100644 --- a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts @@ -238,5 +238,21 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { .withContext('importmap must precede the first module script tag') .toBeLessThan(moduleScriptIdx); }); + + it(`preserves integrity and crossorigin in autoCsp loader when autoCsp is also enabled`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + subresourceIntegrity: true, + security: { autoCsp: true }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const indexHtml = harness.readFile('dist/browser/index.html'); + expect(indexHtml).toMatch( + /const scripts = \[\[(?:'[^']+', 'module', false, false, 'sha384-[^']+', 'anonymous'(?:, )?)+\]\];/, + ); + }); }); }); 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 0e1dfe3ed916..7486b5fa8179 100644 --- a/packages/angular/build/src/utils/index-file/auto-csp.ts +++ b/packages/angular/build/src/utils/index-file/auto-csp.ts @@ -22,6 +22,8 @@ interface SrcScriptTag { type?: string; async: boolean; defer: boolean; + integrity?: string; + crossOrigin?: string; } /** @@ -106,11 +108,14 @@ export async function autoCsp(html: string, unsafeEval = false): Promise // If there are any interesting attributes, note them down. const scriptType = getScriptAttributeValue(tag, 'type'); if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { + const crossOrigin = getScriptAttributeValue(tag, 'crossorigin'); scriptContent.push({ src: src, type: scriptType, async: getScriptAttributeValue(tag, 'async') !== undefined, defer: getScriptAttributeValue(tag, 'defer') !== undefined, + integrity: getScriptAttributeValue(tag, 'integrity'), + crossOrigin: crossOrigin === '' ? 'anonymous' : crossOrigin, }); return; // Skip writing my script tag until we've read it all. @@ -267,36 +272,50 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false) 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 + "'" : "''"; - const asyncAttr = s.async ? 'true' : 'false'; - const deferAttr = s.defer ? 'true' : 'false'; + const asyncAttr = !!s.async; + const deferAttr = !!s.defer; + const integrityAttr = s.integrity ? "'" + s.integrity.replaceAll("'", "\\'") + "'" : null; + const crossOriginAttr = s.crossOrigin ? `'${s.crossOrigin.replaceAll("'", "\\'")}'` : null; - return `['${srcAttr}', ${typeAttr}, ${asyncAttr}, ${deferAttr}]`; + return `['${srcAttr}', ${typeAttr}, ${asyncAttr}, ${deferAttr}, ${integrityAttr}, ${crossOriginAttr}]`; }) .join(); return enableTrustedTypes ? ` - var scripts = [${srcListFormatted}]; - var policy = self.trustedTypes && self.trustedTypes.createPolicy ? + const scripts = [${srcListFormatted}]; + const policy = self.trustedTypes && self.trustedTypes.createPolicy ? self.trustedTypes.createPolicy('angular#auto-csp', {createScriptURL: function(u) { - return scripts.includes(u) ? u : null; + return scripts.some(function(s) { return s[0] === u; }) ? u : null; }}) : { createScriptURL: function(u) { return u; } }; scripts.forEach(function(scriptUrl) { - var s = document.createElement('script'); + const s = document.createElement('script'); s.src = policy.createScriptURL(scriptUrl[0]); s.type = scriptUrl[1]; - s.async = !!scriptUrl[2]; - s.defer = !!scriptUrl[3]; + s.async = scriptUrl[2]; + s.defer = scriptUrl[3]; + if (scriptUrl[4]) { + s.integrity = scriptUrl[4]; + } + if (scriptUrl[5]) { + s.crossOrigin = scriptUrl[5]; + } document.lastElementChild.appendChild(s); });\n` : ` - var scripts = [${srcListFormatted}]; + const scripts = [${srcListFormatted}]; scripts.forEach(function(scriptUrl) { - var s = document.createElement('script'); + const s = document.createElement('script'); s.src = scriptUrl[0]; s.type = scriptUrl[1]; - s.async = !!scriptUrl[2]; - s.defer = !!scriptUrl[3]; + s.async = scriptUrl[2]; + s.defer = scriptUrl[3]; + if (scriptUrl[4]) { + s.integrity = scriptUrl[4]; + } + if (scriptUrl[5]) { + s.crossOrigin = scriptUrl[5]; + } document.lastElementChild.appendChild(s); });\n`; } 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 efbd338233ad..1b892b13091c 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(`var scripts = [['./main.js', '', false, false]];`); + 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* +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', 'module', false, false, 'sha384-xyz123', 'anonymous']];`, + ); + }); + + it('should preserve only integrity attribute when crossorigin is omitted', async () => { + const result = await autoCsp(` + + + + + +
Some text
+ + + `); + + 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, 'sha384-xyz123', null]];`, + ); + }); + + it('should map empty crossorigin attribute to anonymous', async () => { + const result = await autoCsp(` + + + + + +
Some text
+ + + `); + + 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, 'anonymous']];`, + ); + }); });