FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(@angular/build): encode script type in auto-CSP loader · angular/angular-cli@c1b5154 · GitHub

Repository navigation

Commit c1b5154

Browse files
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. (cherry picked from commit 07e608d)
1 parent 663c580 commit c1b5154

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)

Back | FazBrowse Home | New Git URL