Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions _11ty/apply-csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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(
/<script([^>]*)csp-hash([^>]*)>([\s\S]*?)<\/script>/gi,
(match, p1, p2, p3) => {
const hash = cspHashGen(p3);
hashes.push(quote(hash));
return `<script${p1}csp-hash="${hash}"${p2}>${p3}</script>`;
}
);
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 = /<meta[^>]*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;
Expand Down
84 changes: 84 additions & 0 deletions test/test-csp.js
Original file line number Diff line number Diff line change
@@ -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 = `<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' HASHES">
<script csp-hash>console.log('Inline script 1');</script>
<script csp-hash>
const a = 1;
const b = 2;
console.log(a+b);
</script>
<script src="foo.js" csp-hash></script>
</head>
<body>
<h1>Hello World</h1>
<script>console.log('Regular script');</script>
</body>
</html>`;

const htmlWithSwappedAttributes = `<!doctype html>
<html lang="en">
<head>
<meta content="default-src 'self'; script-src 'self' HASHES" http-equiv="Content-Security-Policy">
<script csp-hash>console.log('Inline script 2');</script>
</head>
<body>
</body>
</html>`;

const htmlWithoutCsp = `<!doctype html>
<html lang="en">
<head>
<!-- CSP commented out -->
<script csp-hash>console.log('Inline script 1');</script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`;

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(/<meta[^>]*http-equiv=["']Content-Security-Policy["'][^>]*>/i)[0];
assert.ok(metaTag.includes("'sha256-"), "Meta tag should contain sha256 hashes");

assert.ok(output.includes('<script csp-hash="'), "Script tag should have csp-hash attribute with value");
assert.ok(output.includes("console.log('Inline script 1');"), "Script content should be preserved");
});

it("should handle swapped attributes in meta tag", async () => {
const output = await addCspHashFn(htmlWithSwappedAttributes, "test.html");

assert.strictEqual(output.includes("HASHES"), false, "HASHES placeholder should be replaced");

const metaTag = output.match(/<meta[^>]*http-equiv=["']Content-Security-Policy["'][^>]*>/i)[0];
assert.ok(metaTag.includes("'sha256-"), "Meta tag should contain sha256 hashes");
});

it("should not modify content if CSP meta tag is missing", async () => {
const output = await addCspHashFn(htmlWithoutCsp, "test.html");
assert.strictEqual(output, htmlWithoutCsp, "Output should match input");
});
});