daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

commit c9f14c3cae1408872b1307f747e09b8fa087893c
parent 8f84de47ecb40d0fa19df3087342a2a481aab59e
Author: $: DAΞMON <zer0sec.xp@icloud.com>
Date:   Wed, 30 Sep 2026 05:56:42 +0100

Allow inline scripts by hash instead of 'unsafe-inline'

The CSP admitted any inline script. The site carries four: the theme
and page-iris bootstraps in Base.astro and two small modules Astro
inlines. scripts/csp-hashes.mjs now runs last in the build, hashes
every inline script in dist and writes the hashes into dist/_headers
in place of 'unsafe-inline', so the policy admits those exact scripts
and nothing else. public/_headers is the template. Astro rewrites the
inlined modules on every build, which is why the hashes are computed
rather than written by hand.

A side effect worth knowing: one imported HackTricks sheet renders XSS
demo payloads as real onerror attributes. Those are now blocked too.

Also takes npm audit's non-breaking fixes (js-yaml, svgo, devalue).
The remaining advisories need Astro 7, which is a separate change.

Tested: npm test (build + 4/4). Under wrangler dev with the built dist,
the theme bootstrap ran, search through the modal returned results, a
navigation ran the iris, the PDF page framed its PDF, all with zero CSP
violations on the console.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mpackage-lock.json | 44++++++++++++++++++++++----------------------
Mpackage.json | 4++--
Mpublic/_headers | 18++++++++++++------
Ascripts/csp-hashes.mjs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 94 insertions(+), 30 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -2963,16 +2963,16 @@ } }, "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, "funding": { "url": "https://github.com/sponsors/fb55" @@ -2992,9 +2992,9 @@ } }, "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -3121,9 +3121,9 @@ } }, "node_modules/devalue": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz", - "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", "license": "MIT" }, "node_modules/devlop": { @@ -3963,9 +3963,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -6017,18 +6017,18 @@ } }, "node_modules/svgo": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", - "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", "license": "MIT", "dependencies": { "commander": "^11.1.0", - "css-select": "^5.1.0", + "css-select": "^6.0.0", "css-tree": "^3.0.1", - "css-what": "^6.1.0", + "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", - "sax": "^1.5.0" + "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" diff --git a/package.json b/package.json @@ -3,10 +3,10 @@ "type": "module", "version": "1.0.0", "private": true, - "description": "DÆMON//SEC — a curated vault of IT & cybersecurity cheatsheets", + "description": "D\u00c6MON//SEC \u2014 a curated vault of IT & cybersecurity cheatsheets", "scripts": { "dev": "astro dev", - "build": "astro build && pagefind --site dist && npm run pagefind:public", + "build": "astro build && pagefind --site dist && npm run pagefind:public && node scripts/csp-hashes.mjs", "test": "astro build && node --test", "preview": "astro preview", "pagefind:public": "rm -rf public/pagefind && cp -R dist/pagefind public/pagefind", diff --git a/public/_headers b/public/_headers @@ -3,15 +3,21 @@ # dist/ verbatim, so this is where it is authored. Format and limits: # developers.cloudflare.com/workers/static-assets/headers/ # -# Mirrors daemon-sec's staticSecurityHeaders() with three differences -# this site needs: 'wasm-unsafe-eval' and a same-origin worker for the -# Pagefind search index, 'unsafe-inline' scripts for the theme and page -# iris bootstraps in Base.astro, and data: fonts for the icon face inlined -# in the CSS. No script, style or font is loaded from another origin. +# Mirrors daemon-sec's staticSecurityHeaders() with the differences this +# site needs: 'wasm-unsafe-eval' and a same-origin worker for the Pagefind +# search index, and data: fonts for the icon face inlined in the CSS. No +# script, style or font is loaded from another origin. +# +# Inline scripts (the theme and page-iris bootstraps, and the modules +# Astro inlines) are allowed by hash, not by 'unsafe-inline'. The +# placeholder below is filled by scripts/csp-hashes.mjs at the end of +# `npm run build`, so this file is a template: the served copy is +# dist/_headers. Styles keep 'unsafe-inline' because Astro emits style +# attributes, which no hash can cover. # img-src is open to https: because the imported sheets hotlink figures from # dozens of hosts; frame-src 'self' is for the PDF viewer on the JJ guide. /* - Content-Security-Policy: default-src 'self'; base-uri 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data:; object-src 'none'; media-src 'self'; frame-src 'self'; frame-ancestors 'none'; worker-src 'self'; upgrade-insecure-requests + Content-Security-Policy: default-src 'self'; base-uri 'self'; form-action 'self'; script-src 'self' __INLINE_SCRIPT_HASHES__ 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data:; object-src 'none'; media-src 'self'; frame-src 'self'; frame-ancestors 'none'; worker-src 'self'; upgrade-insecure-requests X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin diff --git a/scripts/csp-hashes.mjs b/scripts/csp-hashes.mjs @@ -0,0 +1,58 @@ +// Fills the inline-script hashes into dist/_headers after a build. +// +// The Content-Security-Policy in public/_headers allows no 'unsafe-inline' +// for scripts. The inline scripts the site does carry (the theme and page +// iris bootstraps in Base.astro, and the small modules Astro inlines) are +// allowed by SHA-256 hash instead, which a browser accepts only for the +// exact text. Astro rewrites the inlined modules on every build, so the +// hashes are computed from the built HTML rather than written by hand. +// +// Runs last in `npm run build`. Anything that adds an inline <script> to a +// page is covered automatically; an inline script that is NOT in dist at +// build time (injected at runtime) will be refused by the browser. +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const DIST = 'dist'; +const HEADERS = join(DIST, '_headers'); +const TOKEN = '__INLINE_SCRIPT_HASHES__'; + +function* htmlFiles(dir) { + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) yield* htmlFiles(p); + else if (name.endsWith('.html')) yield p; + } +} + +// A <script> with no src and a JavaScript type. JSON blocks (JSON-LD) are +// data the browser never executes, so they need no hash. +const SCRIPT = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi; +const isData = (attrs) => /\btype\s*=\s*["']?(application\/(ld\+)?json|text\/(template|x-|plain))/i.test(attrs); + +const hashes = new Set(); +let files = 0; +for (const file of htmlFiles(DIST)) { + files++; + const html = readFileSync(file, 'utf8'); + for (const [, attrs, body] of html.matchAll(SCRIPT)) { + if (/\bsrc\s*=/i.test(attrs) || isData(attrs)) continue; + hashes.add(`'sha256-${createHash('sha256').update(body).digest('base64')}'`); + } +} + +const headers = readFileSync(HEADERS, 'utf8'); +if (!headers.includes(TOKEN)) { + console.error(`csp-hashes: ${HEADERS} has no ${TOKEN} placeholder`); + process.exit(1); +} +const out = headers.replace(TOKEN, [...hashes].join(' ')); +const longest = Math.max(...out.split('\n').map((l) => l.length)); +if (longest > 2000) { + console.error(`csp-hashes: a _headers line is ${longest} chars; Cloudflare's limit is 2000`); + process.exit(1); +} +writeFileSync(HEADERS, out); +console.log(`csp-hashes: ${hashes.size} inline script hash(es) from ${files} pages -> ${HEADERS} (longest line ${longest})`);