115 lines
8.3 KiB
JavaScript
115 lines
8.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { createDeploymentHttpServer, listenDeploymentHttpServer, loadDeploymentContract } from "./deployment-http-server.mjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const reportPath = path.join(root, "tests/golden/M13-05B/csp-resource-report.json");
|
|
const manifestPath = path.join(root, "tests/golden/M13-05B/manifest.json");
|
|
const hashFile = async (file) => crypto.createHash("sha256").update(await fs.promises.readFile(file)).digest("hex");
|
|
const contract = loadDeploymentContract();
|
|
const policy = contract.responseHeaders.allResponses["Content-Security-Policy"];
|
|
const directives = new Map(policy.split(";").map((directive) => {
|
|
const tokens = directive.trim().split(/\s+/u);
|
|
return [tokens.shift(), tokens];
|
|
}));
|
|
const tokens = (name) => directives.get(name) ?? [];
|
|
const sameOriginOnly = ["worker-src", "font-src", "img-src", "media-src"];
|
|
assert.deepEqual(tokens("script-src"), ["'self'", "'wasm-unsafe-eval'"]);
|
|
assert.deepEqual(tokens("worker-src"), ["'self'"]);
|
|
assert.deepEqual(tokens("font-src"), ["'self'"]);
|
|
assert.deepEqual(tokens("img-src"), ["'self'"]);
|
|
assert.deepEqual(tokens("media-src"), ["'self'"]);
|
|
for (const name of sameOriginOnly) for (const forbidden of ["data:", "blob:", "*"]) assert.equal(tokens(name).includes(forbidden), false, `${name} contains ${forbidden}`);
|
|
for (const forbidden of ["'unsafe-inline'", "'unsafe-eval'", "data:", "blob:", "*"]) {
|
|
assert.equal([...directives.values()].flat().includes(forbidden), false, `CSP contains forbidden token ${forbidden}`);
|
|
}
|
|
|
|
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "m13-05b-csp-"));
|
|
const onePixelPng = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64");
|
|
const wav = Buffer.alloc(48);
|
|
wav.write("RIFF", 0); wav.writeUInt32LE(40, 4); wav.write("WAVEfmt ", 8); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(8000, 28); wav.writeUInt16LE(1, 32); wav.writeUInt16LE(8, 34); wav.write("data", 36); wav.writeUInt32LE(4, 40); wav.fill(128, 44);
|
|
const fontSource = path.join(root, "blender/release/datafiles/fonts/Inter.woff2");
|
|
assert.ok(fs.existsSync(fontSource), "font fixture is missing");
|
|
const write = (relative, value) => { const file = path.join(fixtureRoot, relative); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, value); };
|
|
write("index.html", "<!doctype html><title>CSP resource matrix</title><script type=module src=/app.js></script>");
|
|
write("app.js", `
|
|
const result = { worker: false, wasm: false, image: false, font: false, media: false, dataImageBlocked: false, blobWorkerBlocked: false, crossOriginBlocked: false };
|
|
const violations = [];
|
|
document.addEventListener("securitypolicyviolation", (event) => {
|
|
violations.push({ directive: event.effectiveDirective, blockedURI: event.blockedURI });
|
|
if (event.effectiveDirective === "worker-src" && (event.blockedURI === "blob" || event.blockedURI.startsWith("blob:"))) result.blobWorkerBlocked = true;
|
|
if (event.effectiveDirective === "connect-src") result.crossOriginBlocked = true;
|
|
});
|
|
const worker = new Worker("/worker.js", { type: "module" });
|
|
worker.onmessage = () => { result.worker = true; worker.terminate(); };
|
|
fetch("/engine.wasm").then((response) => WebAssembly.instantiateStreaming(response)).then(() => { result.wasm = true; }).catch(() => {});
|
|
const image = new Image(); image.onload = () => { result.image = true; }; image.src = "/pixel.png";
|
|
const audio = new Audio(); audio.addEventListener("loadstart", () => { result.media = true; }, { once: true }); audio.src = "/tone.wav"; audio.load();
|
|
new FontFace("CSPMatrix", "url(/font.woff2)").load().then(() => { result.font = true; }).catch(() => {});
|
|
const blockedImage = new Image(); blockedImage.onerror = () => { result.dataImageBlocked = true; }; blockedImage.src = "data:image/png;base64,iVBORw0KGgo=";
|
|
try { new Worker(URL.createObjectURL(new Blob(["postMessage('unexpected')"], { type: "text/javascript" }))); } catch { result.blobWorkerBlocked = true; }
|
|
fetch("http://127.0.0.1:9/csp-cross-origin").catch(() => { result.crossOriginBlocked = true; });
|
|
setTimeout(() => { window.__cspResourceResult = { result, violations }; }, 500);
|
|
`);
|
|
write("worker.js", "postMessage('worker-ok');\n");
|
|
write("engine.wasm", Buffer.from([0, 97, 115, 109, 1, 0, 0, 0]));
|
|
write("pixel.png", onePixelPng);
|
|
write("tone.wav", wav);
|
|
fs.copyFileSync(fontSource, path.join(fixtureRoot, "font.woff2"));
|
|
const server = createDeploymentHttpServer({ root: fixtureRoot, contract });
|
|
const origin = await listenDeploymentHttpServer(server);
|
|
const expected = new Map([
|
|
["/worker.js", ".js"], ["/engine.wasm", ".wasm"], ["/font.woff2", ".woff2"], ["/pixel.png", ".png"], ["/tone.wav", ".wav"],
|
|
]);
|
|
try {
|
|
for (const [pathname, extension] of expected) {
|
|
const response = await fetch(`${origin}${pathname}`);
|
|
assert.equal(response.status, 200, pathname);
|
|
assert.equal(response.headers.get("content-security-policy"), policy, `CSP mismatch ${pathname}`);
|
|
assert.equal(response.headers.get("content-type"), contract.mimeTypes[extension], `MIME mismatch ${pathname}`);
|
|
assert.ok(Number(response.headers.get("content-length")) > 0, `empty resource ${pathname}`);
|
|
}
|
|
for (const pathname of ["/missing", "/index.html"]) {
|
|
const response = await fetch(`${origin}${pathname}`);
|
|
assert.equal(response.headers.get("content-security-policy"), policy, `CSP missing on ${pathname}`);
|
|
}
|
|
|
|
const { chromium } = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href);
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage();
|
|
await page.goto(`${origin}/`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(700);
|
|
const browserReport = await page.evaluate(() => window.__cspResourceResult);
|
|
await browser.close();
|
|
assert.ok(browserReport, "browser CSP report missing");
|
|
assert.equal(browserReport.result.worker, true);
|
|
assert.equal(browserReport.result.wasm, true);
|
|
assert.equal(browserReport.result.image, true);
|
|
assert.equal(browserReport.result.media, true);
|
|
assert.equal(browserReport.result.dataImageBlocked, true);
|
|
assert.equal(browserReport.result.blobWorkerBlocked, true);
|
|
assert.equal(browserReport.result.crossOriginBlocked, true);
|
|
assert.ok(browserReport.violations.some(({ directive }) => directive === "img-src"));
|
|
assert.ok(browserReport.violations.some(({ directive }) => directive === "worker-src"));
|
|
assert.ok(browserReport.violations.some(({ directive }) => directive === "connect-src"));
|
|
const report = {
|
|
schemaVersion: 1, task: "M13-05B", operation: "CSP_RESOURCE_POLICY", policy,
|
|
resources: { worker: "SAME_ORIGIN", wasm: "SAME_ORIGIN_WASM_UNSAFE_EVAL", font: "SAME_ORIGIN", image: "SAME_ORIGIN", media: "SAME_ORIGIN" },
|
|
denied: { dataImage: "DENY", blobWorker: "DENY", crossOriginConnect: "DENY" },
|
|
responseCount: expected.size + 2, browser: browserReport, execution: "DISABLED", nextTask: "M13-05C",
|
|
};
|
|
if (process.env.UPDATE_M13_05B_REPORT === "1") { await fs.promises.mkdir(path.dirname(reportPath), { recursive: true }); await fs.promises.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); }
|
|
assert.deepEqual(JSON.parse(await fs.promises.readFile(reportPath, "utf8")), report);
|
|
const manifest = JSON.parse(await fs.promises.readFile(manifestPath, "utf8"));
|
|
assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05B", parentTask: "M13-05A", nextTask: "M13-05C" });
|
|
for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`);
|
|
process.stdout.write("csp-resource-policy-ok worker=SAME_ORIGIN wasm=SAME_ORIGIN font=SAME_ORIGIN image=SAME_ORIGIN media=SAME_ORIGIN denied=data,blob,cross-origin execution=DISABLED next=M13-05C\n");
|
|
} finally {
|
|
if (server.listening) await new Promise((resolve) => server.close(resolve));
|
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
}
|