Skip to content

Commit 8dec0c6

Browse files
alan-agius4clydin
authored andcommitted
fix(@angular/build): normalize line endings for CSP hash generation
When `autoCsp` reads an `index.html` with CRLF line endings, it generates hashes based on the CRLF content. However, the transformed file is always written with LF line endings, causing CSP violations. This commit ensures that script content line endings are normalized to LF before hashing to match the output file. Closes #32709 (cherry picked from commit 6324133)
1 parent 79619a1 commit 8dec0c6

File tree

2 files changed

+42
-24
lines changed

2 files changed

+42
-24
lines changed

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,7 @@ function isJavascriptMimeType(mimeType: string): boolean {
5252
* @returns whether to add the script tag to the dynamically loaded script tag
5353
*/
5454
function shouldDynamicallyLoadScriptTagBasedOnType(scriptType: string | undefined): boolean {
55-
return (
56-
scriptType === undefined ||
57-
scriptType === '' ||
58-
scriptType === 'module' ||
59-
isJavascriptMimeType(scriptType)
60-
);
55+
return !scriptType || scriptType === 'module' || isJavascriptMimeType(scriptType);
6156
}
6257

6358
/**
@@ -67,7 +62,11 @@ function shouldDynamicallyLoadScriptTagBasedOnType(scriptType: string | undefine
6762
* @returns The hash of the text formatted appropriately for CSP.
6863
*/
6964
export function hashTextContent(scriptText: string): string {
70-
const hash = crypto.createHash(HASH_FUNCTION).update(scriptText, 'utf-8').digest('base64');
65+
// Normalize CRLF to LF to ensure consistent since the rewriter might normalize the line endings.
66+
const hash = crypto
67+
.createHash(HASH_FUNCTION)
68+
.update(scriptText.replace(/\r\n?/g, '\n'), 'utf-8')
69+
.digest('base64');
7170

7271
return `'${HASH_FUNCTION}-${hash}'`;
7372
}

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

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ const getCsps = (html: string) => {
1515
).map((m) => m[1]); // Only capture group.
1616
};
1717

18-
const ONE_HASH_CSP =
18+
const CSP_SINGLE_HASH_REGEX =
1919
/script-src 'strict-dynamic' 'sha256-[^']+' https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
2020

21-
const TWO_HASH_CSP =
21+
const CSP_TWO_HASHES_REGEX =
2222
/script-src 'strict-dynamic' (?:'sha256-[^']+' ){2}https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
2323

24-
const FOUR_HASH_CSP =
24+
const CSP_FOUR_HASHES_REGEX =
2525
/script-src 'strict-dynamic' (?:'sha256-[^']+' ){4}https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
2626

2727
describe('auto-csp', () => {
@@ -38,8 +38,8 @@ describe('auto-csp', () => {
3838
`);
3939

4040
const csps = getCsps(result);
41-
expect(csps.length).toBe(1);
42-
expect(csps[0]).toMatch(ONE_HASH_CSP);
41+
expect(csps).toHaveSize(1);
42+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
4343
expect(csps[0]).toContain(hashTextContent("console.log('foo');"));
4444
});
4545

@@ -56,8 +56,8 @@ describe('auto-csp', () => {
5656
`);
5757

5858
const csps = getCsps(result);
59-
expect(csps.length).toBe(1);
60-
expect(csps[0]).toMatch(ONE_HASH_CSP);
59+
expect(csps).toHaveSize(1);
60+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
6161
expect(result).toContain(`var scripts = [['./main.js', '', false, false]];`);
6262
});
6363

@@ -74,8 +74,8 @@ describe('auto-csp', () => {
7474
`);
7575

7676
const csps = getCsps(result);
77-
expect(csps.length).toBe(1);
78-
expect(csps[0]).toMatch(ONE_HASH_CSP);
77+
expect(csps).toHaveSize(1);
78+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
7979
// Our loader script appears after the HTML text content.
8080
expect(result).toMatch(
8181
/Some text<\/div>\s*<script>\s*var scripts = \[\['.\/main.js', '', false, false\]\];/,
@@ -99,8 +99,8 @@ describe('auto-csp', () => {
9999
`);
100100

101101
const csps = getCsps(result);
102-
expect(csps.length).toBe(1);
103-
expect(csps[0]).toMatch(TWO_HASH_CSP);
102+
expect(csps).toHaveSize(1);
103+
expect(csps[0]).toMatch(CSP_TWO_HASHES_REGEX);
104104
expect(result).toContain(
105105
// eslint-disable-next-line max-len
106106
`var scripts = [['./main1.js', '', false, false],['./main2.js', '', true, false],['./main3.js', 'module', true, true]];`,
@@ -127,8 +127,8 @@ describe('auto-csp', () => {
127127
`);
128128

129129
const csps = getCsps(result);
130-
expect(csps.length).toBe(1);
131-
expect(csps[0]).toMatch(ONE_HASH_CSP);
130+
expect(csps).toHaveSize(1);
131+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
132132
// &amp; encodes correctly
133133
expect(result).toContain(`'/foo&bar'`);
134134
// Impossible to escape a string and create invalid loader JS with a '
@@ -158,9 +158,9 @@ describe('auto-csp', () => {
158158
`);
159159

160160
const csps = getCsps(result);
161-
expect(csps.length).toBe(1);
161+
expect(csps).toHaveSize(1);
162162
// Exactly four hashes for the four scripts that remain (inline, loader, inline, loader).
163-
expect(csps[0]).toMatch(FOUR_HASH_CSP);
163+
expect(csps[0]).toMatch(CSP_FOUR_HASHES_REGEX);
164164
expect(csps[0]).toContain(hashTextContent("console.log('foo');"));
165165
expect(csps[0]).toContain(hashTextContent("console.log('bar');"));
166166
// Loader script for main.js and main2.js appear after 'foo' and before 'bar'.
@@ -190,8 +190,8 @@ describe('auto-csp', () => {
190190
`);
191191

192192
const csps = getCsps(result);
193-
expect(csps.length).toBe(1);
194-
expect(csps[0]).toMatch(ONE_HASH_CSP);
193+
expect(csps).toHaveSize(1);
194+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
195195

196196
expect(result).toContain(
197197
// eslint-disable-next-line max-len
@@ -202,4 +202,23 @@ describe('auto-csp', () => {
202202
// Only one loader script is created.
203203
expect(Array.from(result.matchAll(/<script>/g)).length).toEqual(1);
204204
});
205+
206+
it('should rewrite a single inline script with CRLF', async () => {
207+
const result = await autoCsp(`
208+
<html>
209+
<head>
210+
</head>
211+
<body>
212+
<script>\r\nconsole.log('foo');\r\n</script>
213+
<div>Some text </div>
214+
</body>
215+
</html>\r\n
216+
`);
217+
218+
const csps = getCsps(result);
219+
expect(result).not.toContain(`\r\n`);
220+
expect(csps).toHaveSize(1);
221+
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
222+
expect(csps[0]).toContain(hashTextContent(`\r\nconsole.log('foo');\r\n`));
223+
});
205224
});

0 commit comments

Comments
 (0)