fix: hash inline scripts with data-src attributes

This commit is contained in:
Peter Steinberger
2026-03-23 21:13:31 -07:00
parent 0857447a5d
commit 26365f7daf
2 changed files with 64 additions and 1 deletions

View File

@@ -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(
`<html><script data-src="/app.js">${content}</script></html>`,
);
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");

View File

@@ -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