Skip to content

Commit 251bd9f

Browse files
aaronshimdgp1130
authored andcommittedDec 11, 2024·
fix(@angular/build): Fixing auto-csp edge cases where
- <script> is the last tag before </head> close - .appendChild is called before </head> (because document.body is undefined then) - <script> tags with a src attribute and no specified type attribute should not write <script type="undefined" ...> (cherry picked from commit 210bf4e)
1 parent e20e739 commit 251bd9f

File tree

2 files changed

+45
-13
lines changed

2 files changed

+45
-13
lines changed
 

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export async function autoCsp(html: string, unsafeEval = false): Promise<string>
9292
* loader script to the collection of hashes to add to the <meta> tag CSP.
9393
*/
9494
function emitLoaderScript() {
95-
const loaderScript = createLoaderScript(scriptContent);
95+
const loaderScript = createLoaderScript(scriptContent, /* enableTrustedTypes = */ false);
9696
hashes.push(hashTextContent(loaderScript));
9797
rewriter.emitRaw(`<script>${loaderScript}</script>`);
9898
scriptContent = [];
@@ -152,7 +152,7 @@ export async function autoCsp(html: string, unsafeEval = false): Promise<string>
152152
}
153153
}
154154

155-
if (tag.tagName === 'body' || tag.tagName === 'html') {
155+
if (tag.tagName === 'head' || tag.tagName === 'body' || tag.tagName === 'html') {
156156
// Write the loader script if a string of <script>s were the last opening tag of the document.
157157
if (scriptContent.length > 0) {
158158
emitLoaderScript();
@@ -267,7 +267,7 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false)
267267
// URI encoding means value can't escape string, JS, or HTML context.
268268
const srcAttr = encodeURI(s.src).replaceAll("'", "\\'");
269269
// Can only be 'module' or a JS MIME type or an empty string.
270-
const typeAttr = s.type ? "'" + s.type + "'" : undefined;
270+
const typeAttr = s.type ? "'" + s.type + "'" : "''";
271271
const asyncAttr = s.async ? 'true' : 'false';
272272
const deferAttr = s.defer ? 'true' : 'false';
273273

@@ -288,7 +288,7 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false)
288288
s.type = scriptUrl[1];
289289
s.async = !!scriptUrl[2];
290290
s.defer = !!scriptUrl[3];
291-
document.body.appendChild(s);
291+
document.lastElementChild.appendChild(s);
292292
});\n`
293293
: `
294294
var scripts = [${srcListFormatted}];
@@ -298,6 +298,6 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false)
298298
s.type = scriptUrl[1];
299299
s.async = !!scriptUrl[2];
300300
s.defer = !!scriptUrl[3];
301-
document.body.appendChild(s);
301+
document.lastElementChild.appendChild(s);
302302
});\n`;
303303
}

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

+40-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const getCsps = (html: string) => {
1818
const ONE_HASH_CSP =
1919
/script-src 'strict-dynamic' 'sha256-[^']+' https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
2020

21+
const TWO_HASH_CSP =
22+
/script-src 'strict-dynamic' (?:'sha256-[^']+' ){2}https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
23+
2124
const FOUR_HASH_CSP =
2225
/script-src 'strict-dynamic' (?:'sha256-[^']+' ){4}https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
2326

@@ -55,7 +58,7 @@ describe('auto-csp', () => {
5558
const csps = getCsps(result);
5659
expect(csps.length).toBe(1);
5760
expect(csps[0]).toMatch(ONE_HASH_CSP);
58-
expect(result).toContain(`var scripts = [['./main.js', undefined, false, false]];`);
61+
expect(result).toContain(`var scripts = [['./main.js', '', false, false]];`);
5962
});
6063

6164
it('should rewrite a single source script in place', async () => {
@@ -75,14 +78,15 @@ describe('auto-csp', () => {
7578
expect(csps[0]).toMatch(ONE_HASH_CSP);
7679
// Our loader script appears after the HTML text content.
7780
expect(result).toMatch(
78-
/Some text<\/div>\s*<script>\s*var scripts = \[\['.\/main.js', undefined, false, false\]\];/,
81+
/Some text<\/div>\s*<script>\s*var scripts = \[\['.\/main.js', '', false, false\]\];/,
7982
);
8083
});
8184

8285
it('should rewrite a multiple source scripts with attributes', async () => {
8386
const result = await autoCsp(`
8487
<html>
8588
<head>
89+
<script src="./head.js"></script>
8690
</head>
8791
<body>
8892
<script src="./main1.js"></script>
@@ -96,13 +100,15 @@ describe('auto-csp', () => {
96100

97101
const csps = getCsps(result);
98102
expect(csps.length).toBe(1);
99-
expect(csps[0]).toMatch(ONE_HASH_CSP);
103+
expect(csps[0]).toMatch(TWO_HASH_CSP);
100104
expect(result).toContain(
101105
// eslint-disable-next-line max-len
102-
`var scripts = [['./main1.js', undefined, false, false],['./main2.js', undefined, true, false],['./main3.js', 'module', true, true]];`,
106+
`var scripts = [['./main1.js', '', false, false],['./main2.js', '', true, false],['./main3.js', 'module', true, true]];`,
103107
);
104-
// Only one loader script is created.
105-
expect(Array.from(result.matchAll(/<script>/g)).length).toEqual(1);
108+
// Head loader script is in the head.
109+
expect(result).toContain(`</script></head>`);
110+
// Only two loader scripts are created.
111+
expect(Array.from(result.matchAll(/<script>/g)).length).toEqual(2);
106112
});
107113

108114
it('should rewrite source scripts with weird URLs', async () => {
@@ -160,14 +166,40 @@ describe('auto-csp', () => {
160166
// Loader script for main.js and main2.js appear after 'foo' and before 'bar'.
161167
expect(result).toMatch(
162168
// eslint-disable-next-line max-len
163-
/console.log\('foo'\);<\/script>\s*<script>\s*var scripts = \[\['.\/main.js', undefined, false, false\],\['.\/main2.js', undefined, false, false\]\];[\s\S]*console.log\('bar'\);/,
169+
/console.log\('foo'\);<\/script>\s*<script>\s*var scripts = \[\['.\/main.js', '', false, false\],\['.\/main2.js', '', false, false\]\];[\s\S]*console.log\('bar'\);/,
164170
);
165171
// Loader script for main3.js and main4.js appear after 'bar'.
166172
expect(result).toMatch(
167173
// eslint-disable-next-line max-len
168-
/console.log\('bar'\);<\/script>\s*<script>\s*var scripts = \[\['.\/main3.js', undefined, false, false\],\['.\/main4.js', undefined, false, false\]\];/,
174+
/console.log\('bar'\);<\/script>\s*<script>\s*var scripts = \[\['.\/main3.js', '', false, false\],\['.\/main4.js', '', false, false\]\];/,
169175
);
170176
// Exactly 4 scripts should be left.
171177
expect(Array.from(result.matchAll(/<script>/g)).length).toEqual(4);
172178
});
179+
180+
it('should write a loader script that appends to head', async () => {
181+
const result = await autoCsp(`
182+
<html>
183+
<head>
184+
<script src="./head.js"></script>
185+
</head>
186+
<body>
187+
<div>Some text </div>
188+
</body>
189+
</html>
190+
`);
191+
192+
const csps = getCsps(result);
193+
expect(csps.length).toBe(1);
194+
expect(csps[0]).toMatch(ONE_HASH_CSP);
195+
196+
expect(result).toContain(
197+
// eslint-disable-next-line max-len
198+
`document.lastElementChild.appendChild`,
199+
);
200+
// Head loader script is in the head.
201+
expect(result).toContain(`</script></head>`);
202+
// Only one loader script is created.
203+
expect(Array.from(result.matchAll(/<script>/g)).length).toEqual(1);
204+
});
173205
});

0 commit comments

Comments
 (0)
Please sign in to comment.