mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-15 00:17:46 +00:00
ci(csp): replace 'unsafe-inline' with sha256 hash at build time
Postbuild script computes SHA-256 of every inline <script> in build/index.html and rewrites build/_headers — replacing the script-src 'unsafe-inline' relaxation with the matching hashes. The hash regenerates per build (SvelteKit bootstrap embeds a per-build registration call) so the script must run on every build; chain it into both `npm run build` and `build:gh`. verify-build extended to assert build/_headers script-src no longer contains 'unsafe-inline', so the inject step's output is enforced in CI. style-src 'unsafe-inline' stays — Svelte's `style:` directives emit inline attributes that hashes can't cover.
This commit is contained in:
1 parent
a81695d982
commit
3fd02c14a2
6 files changed
+80
-6
No files matched your search
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Postbuild step: replace `'unsafe-inline'` in build/_headers script-src
|
||||
* with the SHA-256 hash(es) of every inline <script> in
|
||||
* build/index.html. Hash changes per build are expected (the
|
||||
* SvelteKit bootstrap embeds a timestamped registration call), so
|
||||
* this script must run on every build.
|
||||
*
|
||||
* If no inline scripts are present (future SvelteKit could go
|
||||
* src-only), the script removes `'unsafe-inline'` entirely so the
|
||||
* tightest possible CSP ships.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const HEADERS = "build/_headers";
|
||||
const HTML = "build/index.html";
|
||||
const MARKER = `script-src 'self' 'unsafe-inline'`;
|
||||
|
||||
const html = readFileSync(HTML, "utf8");
|
||||
const inlineScripts = [
|
||||
...html.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g),
|
||||
];
|
||||
|
||||
const hashes = inlineScripts.map((m) => {
|
||||
const body = m[1];
|
||||
const digest = createHash("sha256").update(body, "utf8").digest("base64");
|
||||
return `'sha256-${digest}'`;
|
||||
});
|
||||
|
||||
const replacement =
|
||||
hashes.length > 0
|
||||
? `script-src 'self' ${hashes.join(" ")}`
|
||||
: `script-src 'self'`;
|
||||
|
||||
const headers = readFileSync(HEADERS, "utf8");
|
||||
if (!headers.includes(MARKER)) {
|
||||
console.error(
|
||||
`inject-csp-hashes: marker not found in ${HEADERS}.\nLooking for: ${MARKER}\n` +
|
||||
`Either the previous build already replaced it (re-run \`npm run build\` from clean) ` +
|
||||
`or static/_headers no longer contains the relaxed script-src directive.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
writeFileSync(HEADERS, headers.replace(MARKER, replacement), "utf8");
|
||||
console.log(
|
||||
`inject-csp-hashes: replaced 'unsafe-inline' with ${hashes.length} hash(es) in ${HEADERS}.`,
|
||||
);
|
||||
@@ -13,6 +13,7 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
const EXPECTED_INLINE = 1;
|
||||
const HTML_PATH = "build/index.html";
|
||||
const HEADERS_PATH = "build/_headers";
|
||||
|
||||
let html;
|
||||
try {
|
||||
@@ -40,4 +41,26 @@ if (inline < EXPECTED_INLINE) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`verify-build: ${inline} inline <script> tag(s) — OK.`);
|
||||
// Post-Phase 7: script-src must NOT contain 'unsafe-inline' anymore —
|
||||
// `inject-csp-hashes` should have replaced it with sha256 hashes.
|
||||
let headers;
|
||||
try {
|
||||
headers = readFileSync(HEADERS_PATH, "utf8");
|
||||
} catch {
|
||||
console.warn(`verify-build: ${HEADERS_PATH} not found — skipping CSP check.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const scriptSrcLine = headers
|
||||
.split("\n")
|
||||
.find((l) => /script-src\b/.test(l) && /Content-Security-Policy/i.test(l));
|
||||
// Single-line policy: Content-Security-Policy: ... script-src 'self' …
|
||||
if (scriptSrcLine && /script-src[^;]*'unsafe-inline'/.test(scriptSrcLine)) {
|
||||
console.error(
|
||||
`verify-build: ${HEADERS_PATH} script-src still contains 'unsafe-inline'. ` +
|
||||
`inject-csp-hashes.mjs should have replaced it with SHA-256 hash(es).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`verify-build: ${inline} inline <script> tag(s), CSP hashed — OK.`);
|
||||
Reference in new issue
Block a user