Skip to content

Commit 180b192

Browse files
committed
fix(@angular/build): support standard JavaScript MIME types and case insensitivity in auto-CSP
Previously, isJavascriptMimeType() in auto-csp.ts only performed a case-sensitive check against 'text/javascript' on the slice prior to the first semicolon. This caused several issues: 1. Valid and common JavaScript MIME types specified in the HTML Living Standard such as application/javascript and text/ecmascript were not recognized as JavaScript. 2. Case differences (e.g. type="text/JavaScript" or type="Module") were not matched, despite HTML attribute matching and MIME types being ASCII case-insensitive. 3. Whitespace around the essence (e.g. type="text/javascript ; charset=utf-8" or type=" text/javascript") caused the strict equality check to fail. When a script tag with one of these valid types was not recognized, auto-csp bypassed dynamic script rewriting and emitted the script element as-is into index.html. Under the generated strict CSP, the browser would then block the script from executing. This change introduces JAVASCRIPT_MIME_TYPES containing all HTML-standard JavaScript MIME types, normalizes the essence (stripping parameters, trimming surrounding whitespace, and lowercasing), and updates shouldDynamicallyLoadScriptTagBasedOnType to support case-insensitive module types.
1 parent 31c0456 commit 180b192

2 files changed

Lines changed: 151 additions & 5 deletions

File tree

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,40 @@ function getScriptAttributeValue(tag: StartTag, attrName: string): string | unde
3636
return tag.attrs.find((attr) => attr.name === attrName)?.value;
3737
}
3838

39+
/**
40+
* All MIME types associated with JavaScript according to the HTML specification:
41+
* https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type
42+
*/
43+
const JAVASCRIPT_MIME_TYPES = new Set([
44+
'application/ecmascript',
45+
'application/javascript',
46+
'application/x-ecmascript',
47+
'application/x-javascript',
48+
'text/ecmascript',
49+
'text/javascript',
50+
'text/javascript1.0',
51+
'text/javascript1.1',
52+
'text/javascript1.2',
53+
'text/javascript1.3',
54+
'text/javascript1.4',
55+
'text/javascript1.5',
56+
'text/jscript',
57+
'text/livescript',
58+
'text/x-ecmascript',
59+
'text/x-javascript',
60+
]);
61+
3962
/**
4063
* Checks whether a particular string is a MIME type associated with JavaScript, according to
41-
* https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types#textjavascript
64+
* https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type
4265
*
4366
* @param mimeType a string that may be a MIME type
4467
* @returns whether the string is a MIME type that is associated with JavaScript
4568
*/
46-
function isJavascriptMimeType(mimeType: string): boolean {
47-
return mimeType.split(';')[0] === 'text/javascript';
69+
export function isJavascriptMimeType(mimeType: string): boolean {
70+
const [essence] = mimeType.split(';', 1);
71+
72+
return JAVASCRIPT_MIME_TYPES.has(essence.trim().toLowerCase());
4873
}
4974

5075
/**
@@ -54,7 +79,11 @@ function isJavascriptMimeType(mimeType: string): boolean {
5479
* @returns whether to add the script tag to the dynamically loaded script tag
5580
*/
5681
function shouldDynamicallyLoadScriptTagBasedOnType(scriptType: string | undefined): boolean {
57-
return !scriptType || scriptType === 'module' || isJavascriptMimeType(scriptType);
82+
if (!scriptType) {
83+
return true;
84+
}
85+
86+
return scriptType.trim().toLowerCase() === 'module' || isJavascriptMimeType(scriptType);
5887
}
5988

6089
/**

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

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { autoCsp, hashTextContent } from './auto-csp';
9+
import { autoCsp, hashTextContent, isJavascriptMimeType } from './auto-csp';
1010

1111
// Utility function to grab the meta tag CSPs from the HTML response.
1212
const getCsps = (html: string) => {
@@ -281,4 +281,121 @@ describe('auto-csp', () => {
281281
`const scripts = [['./main.js', '', false, false, null, "anonymous"]];`,
282282
);
283283
});
284+
285+
it('should rewrite scripts with application/javascript type', async () => {
286+
const result = await autoCsp(`
287+
<html>
288+
<head></head>
289+
<body>
290+
<script src="./main.js" type="application/javascript"></script>
291+
</body>
292+
</html>
293+
`);
294+
295+
const csps = getCsps(result);
296+
expect(csps).toHaveSize(1);
297+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
298+
expect(result).toContain(
299+
`const scripts = [['./main.js', 'application/javascript', false, false, null, null]];`,
300+
);
301+
});
302+
303+
it('should rewrite scripts with case-insensitive type and parameters with whitespace', async () => {
304+
const result = await autoCsp(`
305+
<html>
306+
<head></head>
307+
<body>
308+
<script src="./main.js" type="Text/JavaScript ; charset=utf-8"></script>
309+
</body>
310+
</html>
311+
`);
312+
313+
const csps = getCsps(result);
314+
expect(csps).toHaveSize(1);
315+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
316+
expect(result).toContain(
317+
`const scripts = [['./main.js', 'Text/JavaScript ; charset=utf-8', false, false, null, null]];`,
318+
);
319+
});
320+
321+
it('should rewrite scripts with case-insensitive module type', async () => {
322+
const result = await autoCsp(`
323+
<html>
324+
<head></head>
325+
<body>
326+
<script src="./main.js" type="Module"></script>
327+
</body>
328+
</html>
329+
`);
330+
331+
const csps = getCsps(result);
332+
expect(csps).toHaveSize(1);
333+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
334+
expect(result).toContain(
335+
`const scripts = [['./main.js', 'Module', false, false, null, null]];`,
336+
);
337+
});
338+
339+
it('should not rewrite non-JavaScript script tags', async () => {
340+
const result = await autoCsp(`
341+
<html>
342+
<head></head>
343+
<body>
344+
<script src="./data.json" type="application/json"></script>
345+
</body>
346+
</html>
347+
`);
348+
349+
// No dynamic loader script is emitted because application/json is not JavaScript.
350+
expect(result).toContain('<script src="./data.json" type="application/json"></script>');
351+
expect(result).not.toContain('const scripts =');
352+
});
353+
354+
describe('isJavascriptMimeType', () => {
355+
it('should identify standard JavaScript MIME types', () => {
356+
expect(isJavascriptMimeType('text/javascript')).toBeTrue();
357+
expect(isJavascriptMimeType('application/javascript')).toBeTrue();
358+
expect(isJavascriptMimeType('application/x-javascript')).toBeTrue();
359+
expect(isJavascriptMimeType('text/ecmascript')).toBeTrue();
360+
expect(isJavascriptMimeType('application/ecmascript')).toBeTrue();
361+
expect(isJavascriptMimeType('text/jscript')).toBeTrue();
362+
expect(isJavascriptMimeType('text/livescript')).toBeTrue();
363+
expect(isJavascriptMimeType('text/x-ecmascript')).toBeTrue();
364+
expect(isJavascriptMimeType('text/x-javascript')).toBeTrue();
365+
expect(isJavascriptMimeType('text/javascript1.5')).toBeTrue();
366+
});
367+
368+
it('should ignore parameters when matching MIME type', () => {
369+
expect(isJavascriptMimeType('text/javascript; charset=utf-8')).toBeTrue();
370+
expect(isJavascriptMimeType('application/javascript;version=1.8')).toBeTrue();
371+
});
372+
373+
it('should handle leading, trailing, and parameter whitespace', () => {
374+
expect(isJavascriptMimeType(' text/javascript ')).toBeTrue();
375+
expect(isJavascriptMimeType('text/javascript ; charset=utf-8')).toBeTrue();
376+
expect(isJavascriptMimeType(' application/javascript ; version=1.0 ')).toBeTrue();
377+
});
378+
379+
it('should be case-insensitive', () => {
380+
expect(isJavascriptMimeType('Text/JavaScript')).toBeTrue();
381+
expect(isJavascriptMimeType('APPLICATION/JAVASCRIPT')).toBeTrue();
382+
expect(isJavascriptMimeType('text/JAVASCRIPT; charset=UTF-8')).toBeTrue();
383+
});
384+
385+
it('should reject non-JavaScript MIME types', () => {
386+
expect(isJavascriptMimeType('application/json')).toBeFalse();
387+
expect(isJavascriptMimeType('text/html')).toBeFalse();
388+
expect(isJavascriptMimeType('text/css')).toBeFalse();
389+
expect(isJavascriptMimeType('image/svg+xml')).toBeFalse();
390+
expect(isJavascriptMimeType('importmap')).toBeFalse();
391+
expect(isJavascriptMimeType('module')).toBeFalse();
392+
expect(isJavascriptMimeType('')).toBeFalse();
393+
});
394+
395+
it('should reject invalid MIME types with whitespace inside the essence', () => {
396+
expect(isJavascriptMimeType('text / javascript')).toBeFalse();
397+
expect(isJavascriptMimeType('application / javascript')).toBeFalse();
398+
expect(isJavascriptMimeType('text/java script')).toBeFalse();
399+
});
400+
});
284401
});

0 commit comments

Comments
 (0)