Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/angular/build/src/utils/index-file/auto-csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,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');
Expand Down
54 changes: 46 additions & 8 deletions packages/angular/build/src/utils/index-file/auto-csp_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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*<script>\(\(\) => {\s*const scripts = \[\['.\/main.js', '', false, false, null, null\]\];/,
/Some text<\/div>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main.js', "", false, false, null, null\]\];/,
);
});

Expand All @@ -103,7 +103,7 @@ describe('auto-csp', () => {
expect(csps[0]).toMatch(CSP_TWO_HASHES_REGEX);
expect(result).toContain(
// eslint-disable-next-line max-len
`const scripts = [['./main1.js', '', false, false, null, null],['./main2.js', '', true, false, null, null],['./main3.js', 'module', true, true, null, null]];`,
`const scripts = [['./main1.js', "", false, false, null, null],['./main2.js', "", true, false, null, null],['./main3.js', "module", true, true, null, null]];`,
);
// Head loader script is in the head.
expect(result).toContain(`</script></head>`);
Expand Down Expand Up @@ -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*<script>\(\(\) => {\s*const scripts = \[\['.\/main.js', '', false, false, null, null\],\['.\/main2.js', '', false, false, null, null\]\];[\s\S]*console.log\('bar'\);/,
/console.log\('foo'\);<\/script>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main.js', "", false, false, null, null\],\['.\/main2.js', "", false, false, null, null\]\];[\s\S]*console.log\('bar'\);/,
);
// Loader script for main3.js and main4.js appear after 'bar'.
expect(result).toMatch(
// eslint-disable-next-line max-len
/console.log\('bar'\);<\/script>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main3.js', '', false, false, null, null\],\['.\/main4.js', '', false, false, null, null\]\];/,
/console.log\('bar'\);<\/script>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main3.js', "", false, false, null, null\],\['.\/main4.js', "", false, false, null, null\]\];/,
);
// Exactly 4 scripts should be left.
expect(Array.from(result.matchAll(/<script>/gi)).length).toEqual(4);
Expand Down Expand Up @@ -238,7 +238,7 @@ describe('auto-csp', () => {
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"]];`,
`const scripts = [['./main.js', "module", false, false, "sha384-xyz123", "anonymous"]];`,
);
});

Expand All @@ -258,7 +258,7 @@ describe('auto-csp', () => {
expect(csps).toHaveSize(1);
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
expect(result).toContain(
`const scripts = [['./main.js', '', false, false, "sha384-xyz123", null]];`,
`const scripts = [['./main.js', "", false, false, "sha384-xyz123", null]];`,
);
});

Expand All @@ -278,7 +278,45 @@ describe('auto-csp', () => {
expect(csps).toHaveSize(1);
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
expect(result).toContain(
`const scripts = [['./main.js', '', false, false, null, "anonymous"]];`,
`const scripts = [['./main.js', "", false, false, null, "anonymous"]];`,
);
});

it('should encode a script type that carries MIME parameters', async () => {
const result = await autoCsp(`
<html>
<head>
</head>
<body>
<script src="./main.js" type="text/javascript;']];var x=1;var junk=[['a','b"></script>
</body>
</html>
`);

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(`
<html>
<head>
</head>
<body>
<script src="./main.js" type="text/javascript;</script><script>x</script>"></script>
</body>
</html>
`);

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(/<script/gi)).length).toEqual(1);
});
});