diff --git a/src/gateway/control-ui-csp.test.ts b/src/gateway/control-ui-csp.test.ts index cf5435c6c30..56b7ce2d593 100644 --- a/src/gateway/control-ui-csp.test.ts +++ b/src/gateway/control-ui-csp.test.ts @@ -55,6 +55,15 @@ describe("computeInlineScriptHashes", () => { expect(hashes).toEqual([]); }); + it("does not treat data-src as an external script attribute", () => { + const content = "console.log('inline')"; + const expected = createHash("sha256").update(content, "utf8").digest("base64"); + const hashes = computeInlineScriptHashes( + ``, + ); + expect(hashes).toEqual([`sha256-${expected}`]); + }); + it("hashes only inline scripts when mixed with external", () => { const inlineContent = "console.log('init')"; const expected = createHash("sha256").update(inlineContent, "utf8").digest("base64"); diff --git a/src/gateway/control-ui-csp.ts b/src/gateway/control-ui-csp.ts index c6e8c53bf31..9cfb60bf552 100644 --- a/src/gateway/control-ui-csp.ts +++ b/src/gateway/control-ui-csp.ts @@ -10,7 +10,7 @@ export function computeInlineScriptHashes(html: string): string[] { let match: RegExpExecArray | null; while ((match = re.exec(html)) !== null) { const openTag = match[0].slice(0, match[0].indexOf(">") + 1); - if (/\bsrc\s*=/i.test(openTag)) { + if (hasScriptSrcAttribute(openTag)) { continue; } const content = match[1]; @@ -23,6 +23,60 @@ export function computeInlineScriptHashes(html: string): string[] { return hashes; } +function hasScriptSrcAttribute(openTag: string): boolean { + let i = openTag.search(/\bscript\b/i); + if (i < 0) { + return false; + } + i += "script".length; + while (i < openTag.length) { + while (i < openTag.length && /\s/.test(openTag[i] ?? "")) { + i += 1; + } + const current = openTag[i]; + if (!current || current === ">") { + return false; + } + if (current === "/") { + i += 1; + continue; + } + const nameStart = i; + while (i < openTag.length && /[^\s=/>]/.test(openTag[i] ?? "")) { + i += 1; + } + const attributeName = openTag.slice(nameStart, i).toLowerCase(); + if (attributeName === "src") { + return true; + } + while (i < openTag.length && /\s/.test(openTag[i] ?? "")) { + i += 1; + } + if ((openTag[i] ?? "") !== "=") { + continue; + } + i += 1; + while (i < openTag.length && /\s/.test(openTag[i] ?? "")) { + i += 1; + } + const quote = openTag[i]; + if (quote === '"' || quote === "'") { + i += 1; + while (i < openTag.length && openTag[i] !== quote) { + i += 1; + } + if (openTag[i] === quote) { + i += 1; + } + continue; + } + while (i < openTag.length && /[^\s>]/.test(openTag[i] ?? "")) { + i += 1; + } + } + return false; +} + export function buildControlUiCspHeader(opts?: { inlineScriptHashes?: string[] }): string { const hashes = opts?.inlineScriptHashes; const scriptSrc = hashes?.length