Skip to content

Commit 07e608d

Browse files
yoggydevdgp1130
authored andcommitted
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;<parameters> 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.
1 parent 0530944 commit 07e608d

2 files changed

Lines changed: 53 additions & 13 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,10 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false)
299299
.map((s) => {
300300
// URI encoding means value can't escape string, JS, or HTML context.
301301
const srcAttr = encodeURI(s.src).replaceAll("'", "\\'");
302-
// Can only be 'module' or a JS MIME type or an empty string.
303-
const typeAttr = s.type ? "'" + s.type + "'" : "''";
302+
// 'module', a JS MIME type, or an empty string. A JS MIME type may carry
303+
// parameters after a ';', which isJavascriptMimeType() does not constrain,
304+
// so encode this the same way as integrity and crossOrigin below.
305+
const typeAttr = JSON.stringify(s.type ?? '').replaceAll('<', '\\u003c');
304306
const asyncAttr = !!s.async;
305307
const deferAttr = !!s.defer;
306308
const integrityAttr = JSON.stringify(s.integrity ?? null).replaceAll('<', '\\u003c');

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

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('auto-csp', () => {
5858
const csps = getCsps(result);
5959
expect(csps).toHaveSize(1);
6060
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
61-
expect(result).toContain(`const scripts = [['./main.js', '', false, false, null, null]];`);
61+
expect(result).toContain(`const scripts = [['./main.js', "", false, false, null, null]];`);
6262
});
6363

6464
it('should rewrite a single source script in place', async () => {
@@ -78,7 +78,7 @@ describe('auto-csp', () => {
7878
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
7979
// Our loader script appears after the HTML text content.
8080
expect(result).toMatch(
81-
/Some text<\/div>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main.js', '', false, false, null, null\]\];/,
81+
/Some text<\/div>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main.js', "", false, false, null, null\]\];/,
8282
);
8383
});
8484

@@ -103,7 +103,7 @@ describe('auto-csp', () => {
103103
expect(csps[0]).toMatch(CSP_TWO_HASHES_REGEX);
104104
expect(result).toContain(
105105
// eslint-disable-next-line max-len
106-
`const scripts = [['./main1.js', '', false, false, null, null],['./main2.js', '', true, false, null, null],['./main3.js', 'module', true, true, null, null]];`,
106+
`const scripts = [['./main1.js', "", false, false, null, null],['./main2.js', "", true, false, null, null],['./main3.js', "module", true, true, null, null]];`,
107107
);
108108
// Head loader script is in the head.
109109
expect(result).toContain(`</script></head>`);
@@ -166,12 +166,12 @@ describe('auto-csp', () => {
166166
// Loader script for main.js and main2.js appear after 'foo' and before 'bar'.
167167
expect(result).toMatch(
168168
// eslint-disable-next-line max-len
169-
/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'\);/,
169+
/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'\);/,
170170
);
171171
// Loader script for main3.js and main4.js appear after 'bar'.
172172
expect(result).toMatch(
173173
// eslint-disable-next-line max-len
174-
/console.log\('bar'\);<\/script>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main3.js', '', false, false, null, null\],\['.\/main4.js', '', false, false, null, null\]\];/,
174+
/console.log\('bar'\);<\/script>\s*<script>\(\(\) => {\s*const scripts = \[\['.\/main3.js', "", false, false, null, null\],\['.\/main4.js', "", false, false, null, null\]\];/,
175175
);
176176
// Exactly 4 scripts should be left.
177177
expect(Array.from(result.matchAll(/<script>/gi)).length).toEqual(4);
@@ -238,7 +238,7 @@ describe('auto-csp', () => {
238238
expect(csps).toHaveSize(1);
239239
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
240240
expect(result).toContain(
241-
`const scripts = [['./main.js', 'module', false, false, "sha384-xyz123", "anonymous"]];`,
241+
`const scripts = [['./main.js', "module", false, false, "sha384-xyz123", "anonymous"]];`,
242242
);
243243
});
244244

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

@@ -278,7 +278,7 @@ describe('auto-csp', () => {
278278
expect(csps).toHaveSize(1);
279279
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
280280
expect(result).toContain(
281-
`const scripts = [['./main.js', '', false, false, null, "anonymous"]];`,
281+
`const scripts = [['./main.js', "", false, false, null, "anonymous"]];`,
282282
);
283283
});
284284

@@ -296,7 +296,7 @@ describe('auto-csp', () => {
296296
expect(csps).toHaveSize(1);
297297
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
298298
expect(result).toContain(
299-
`const scripts = [['./main.js', 'application/javascript', false, false, null, null]];`,
299+
`const scripts = [['./main.js', "application/javascript", false, false, null, null]];`,
300300
);
301301
});
302302

@@ -314,7 +314,7 @@ describe('auto-csp', () => {
314314
expect(csps).toHaveSize(1);
315315
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
316316
expect(result).toContain(
317-
`const scripts = [['./main.js', 'Text/JavaScript ; charset=utf-8', false, false, null, null]];`,
317+
`const scripts = [['./main.js', "Text/JavaScript ; charset=utf-8", false, false, null, null]];`,
318318
);
319319
});
320320

@@ -332,7 +332,7 @@ describe('auto-csp', () => {
332332
expect(csps).toHaveSize(1);
333333
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
334334
expect(result).toContain(
335-
`const scripts = [['./main.js', 'Module', false, false, null, null]];`,
335+
`const scripts = [['./main.js', "Module", false, false, null, null]];`,
336336
);
337337
});
338338

@@ -351,6 +351,44 @@ describe('auto-csp', () => {
351351
expect(result).not.toContain('const scripts =');
352352
});
353353

354+
it('should encode a script type that carries MIME parameters', async () => {
355+
const result = await autoCsp(`
356+
<html>
357+
<head>
358+
</head>
359+
<body>
360+
<script src="./main.js" type="text/javascript;']];var x=1;var junk=[['a','b"></script>
361+
</body>
362+
</html>
363+
`);
364+
365+
const csps = getCsps(result);
366+
expect(csps).toHaveSize(1);
367+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
368+
// The type stays inside its string literal.
369+
expect(result).toContain(
370+
`const scripts = [['./main.js', "text/javascript;']];var x=1;var junk=[['a','b", false, false, null, null]];`,
371+
);
372+
});
373+
374+
it('should encode a script type that contains a closing script tag', async () => {
375+
const result = await autoCsp(`
376+
<html>
377+
<head>
378+
</head>
379+
<body>
380+
<script src="./main.js" type="text/javascript;</script><script>x</script>"></script>
381+
</body>
382+
</html>
383+
`);
384+
385+
const csps = getCsps(result);
386+
expect(csps).toHaveSize(1);
387+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
388+
// Only the loader element is emitted.
389+
expect(Array.from(result.matchAll(/<script/gi)).length).toEqual(1);
390+
});
391+
354392
describe('isJavascriptMimeType', () => {
355393
it('should identify standard JavaScript MIME types', () => {
356394
expect(isJavascriptMimeType('text/javascript')).toBeTrue();

0 commit comments

Comments
 (0)