From 3d1bef0635dbaab53255e1281734b4d67c823c0c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:54:26 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Optimize=20CSP=20transform:=20Repla?= =?UTF-8?q?ce=20JSDOM=20with=20Regex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: si <18108+si@users.noreply.github.com> --- _11ty/apply-csp.js | 47 ++++++++++++++------------ test/test-csp.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 test/test-csp.js diff --git a/_11ty/apply-csp.js b/_11ty/apply-csp.js index 5759f96..e855b60 100644 --- a/_11ty/apply-csp.js +++ b/_11ty/apply-csp.js @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const { JSDOM } = require("jsdom"); const cspHashGen = require("csp-hash-generator"); const syncPackage = require("browser-sync/package.json"); @@ -49,32 +48,36 @@ const addCspHash = async (rawContent, outputPath) => { let content = rawContent; if (outputPath && outputPath.endsWith(".html")) { - const dom = new JSDOM(content); - const cspAble = [ - ...dom.window.document.querySelectorAll("script[csp-hash]"), - ]; - - const hashes = cspAble.map((element) => { - const hash = cspHashGen(element.textContent); - element.setAttribute("csp-hash", hash); - return quote(hash); - }); - if (isDevelopmentMode()) { - hashes.push.apply(hashes, AUTO_RELOAD_SCRIPTS); + // Fast fail if no CSP meta tag is present (check for the header value) + if (!/Content-Security-Policy/i.test(content)) { + return content; } - const csp = dom.window.document.querySelector( - "meta[http-equiv='Content-Security-Policy']" + const hashes = []; + + // Find and replace script tags with csp-hash attribute + content = content.replace( + /]*)csp-hash([^>]*)>([\s\S]*?)<\/script>/gi, + (match, p1, p2, p3) => { + const hash = cspHashGen(p3); + hashes.push(quote(hash)); + return `${p3}`; + } ); - if (!csp) { - return content; + + if (isDevelopmentMode()) { + hashes.push.apply(hashes, AUTO_RELOAD_SCRIPTS); } - csp.setAttribute( - "content", - csp.getAttribute("content").replace("HASHES", hashes.join(" ")) - ); - content = dom.serialize(); + // Replace HASHES in the CSP meta tag + // Finds the meta tag with http-equiv="Content-Security-Policy" and replaces HASHES inside it + const metaTagRegex = /]*http-equiv=["']Content-Security-Policy["'][^>]*>/i; + content = content.replace(metaTagRegex, (match) => { + if (match.includes("HASHES")) { + return match.replace("HASHES", hashes.join(" ")); + } + return match; + }); } return content; diff --git a/test/test-csp.js b/test/test-csp.js new file mode 100644 index 0000000..939e506 --- /dev/null +++ b/test/test-csp.js @@ -0,0 +1,84 @@ + +const assert = require("assert"); +const applyCsp = require("../_11ty/apply-csp.js"); + +describe("CSP Transform", () => { + let addCspHashFn; + + before(() => { + const mockEleventyConfig = { + addTransform: (name, fn) => { + if (name === "csp") addCspHashFn = fn; + } + }; + applyCsp.configFunction(mockEleventyConfig); + }); + + const htmlWithCsp = ` + + + + + + + + +

Hello World

+ + +`; + + const htmlWithSwappedAttributes = ` + + + + + + + +`; + + const htmlWithoutCsp = ` + + + + + + +

Hello World

+ +`; + + it("should replace HASHES and populate csp-hash attributes", async () => { + const output = await addCspHashFn(htmlWithCsp, "test.html"); + + assert.strictEqual(output.includes("HASHES"), false, "HASHES placeholder should be replaced"); + + const hashRegex = /csp-hash="[^"]+"/; + assert.ok(hashRegex.test(output), "csp-hash attribute should be populated"); + + const metaTag = output.match(/]*http-equiv=["']Content-Security-Policy["'][^>]*>/i)[0]; + assert.ok(metaTag.includes("'sha256-"), "Meta tag should contain sha256 hashes"); + + assert.ok(output.includes('