From a4b092835f13244fd93cceb18df032e2b136b743 Mon Sep 17 00:00:00 2001 From: Aaron Shim Date: Tue, 15 Oct 2024 08:56:53 +0000 Subject: [PATCH] refactor(@angular/build): Auto-CSP support as an index file transformation. Auto-CSP is a feature to rewrite the ``); + scriptContent = []; + } + + rewriter.on('startTag', (tag, html) => { + if (tag.tagName === 'script') { + openedScriptTag = tag; + const src = getScriptAttributeValue(tag, 'src'); + + if (src) { + // If there are any interesting attributes, note them down. + const scriptType = getScriptAttributeValue(tag, 'type'); + if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { + scriptContent.push({ + src: src, + type: scriptType, + async: getScriptAttributeValue(tag, 'async') !== undefined, + defer: getScriptAttributeValue(tag, 'defer') !== undefined, + }); + return; // Skip writing my script tag until we've read it all. + } + } + } + // We are encountering the first start tag that's not tag if it's a part of the + // dynamic loader script. + if (src && shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { + return; + } + } + + if (tag.tagName === 'body' || tag.tagName === 'html') { + // Write the loader script if a string of +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + expect(csps[0]).toContain(hashTextContent("console.log('foo');")); + }); + + it('should rewrite a single source script', async () => { + const result = await autoCsp(` + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + expect(result).toContain(`var scripts = [['./main.js', undefined, false, false]];`); + }); + + it('should rewrite a single source script in place', async () => { + const result = await autoCsp(` + + + + +
Some text
+ + + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + // Our loader script appears after the HTML text content. + expect(result).toMatch( + /Some text<\/div>\s* + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + expect(result).toContain( + `var scripts = [['./main1.js', undefined, false, false],['./main2.js', undefined, true, false],['./main3.js', 'module', true, true]];`, + ); + // Only one loader script is created. + expect(Array.from(result.matchAll(/\/g)).length).toEqual(1); + }); + + it('should rewrite source scripts with weird URLs', async () => { + const result = await autoCsp(` + + + + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + // & encodes correctly + expect(result).toContain(`'/foo&bar'`); + // Impossible to escape a string and create invalid loader JS with a ' + // (Quotes and backslashes work) + expect(result).toContain(`'/one\\'two%5C\\'three%5C%5C\\'four%5C%5C%5C\\'five'`); + // HTML entities work + expect(result).toContain(`'/one&two&three&four'`); + // Cannot escape JS context to HTML + expect(result).toContain(`'./%3C/script%3E'`); + }); + + it('should rewrite all script tags', async () => { + const result = await autoCsp(` + + + + + + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + // Exactly four hashes for the four scripts that remain (inline, loader, inline, loader). + expect(csps[0]).toMatch(FOUR_HASH_CSP); + expect(csps[0]).toContain(hashTextContent("console.log('foo');")); + expect(csps[0]).toContain(hashTextContent("console.log('bar');")); + // Loader script for main.js and main2.js appear after 'foo' and before 'bar'. + expect(result).toMatch( + /console.log\('foo'\);<\/script>\s*