commit c83e0f33c1d93ecd92a0acab980b27ed3bcda84b
parent 9a8ae72dd6e87bda884f338244705af42b569ee1
Author: $: DAΞMON <zer0sec.xp@icloud.com>
Date: Wed, 30 Sep 2026 05:33:24 +0100
Send security headers from Cloudflare, and drop the eval the CSP forbids
The security headers lived in vercel.json, but the live host is
Cloudflare, which never reads it, so the site shipped with none.
public/_headers now carries the same set daemon-sec serves, adjusted
for what this site loads: WebAssembly and a same-origin worker for
Pagefind, inline scripts for the theme and page-iris bootstraps, and
data: fonts. Nothing here embeds another origin, so images and frames
stay closed. The two Cache-Control rules from vercel.json come along,
since on Cloudflare this file is the only place headers come from.
The search modal built its Pagefind import through new Function to
keep Vite's dev server from rewriting it, which is an eval and the
new policy blocks it. That trick now runs in dev only; the build gets
a plain runtime-string import(), which Rollup leaves native, so no
eval reaches the bundle.
Tested: npm run check (0 errors), npm test (12/12). Under wrangler dev
with the built dist, every response carried the headers, the cache
rules matched their paths, and search through the modal returned
results with zero CSP violations on the page-load console.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -14,3 +14,6 @@ dist/
.vercel/
.vercel
.env*
+
+# wrangler dev scratch state
+.wrangler/
diff --git a/public/_headers b/public/_headers
@@ -0,0 +1,30 @@
+# Security headers for every static response Cloudflare serves. Workers
+# reads this file out of the asset directory; Astro copies public/ into
+# 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.
+# Nothing on this site embeds another origin, so img-src and frame-src stay
+# closed. frame-ancestors 'self' matches vercel.json, the Vercel-side twin.
+/*
+ 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:; connect-src 'self'; font-src 'self' data:; object-src 'none'; media-src 'self'; frame-src 'none'; frame-ancestors 'self'; worker-src 'self'; upgrade-insecure-requests
+ X-Frame-Options: SAMEORIGIN
+ X-Content-Type-Options: nosniff
+ Referrer-Policy: strict-origin-when-cross-origin
+ Cross-Origin-Opener-Policy: same-origin
+ Strict-Transport-Security: max-age=31536000; includeSubDomains
+ Permissions-Policy: camera=(), microphone=(), geolocation=(), browsing-topics=()
+ X-DNS-Prefetch-Control: off
+ X-Permitted-Cross-Domain-Policies: none
+
+# Cache rules carried over from vercel.json: the search index is content-
+# hashed, the dataset is not.
+/data/index-*.json
+ Cache-Control: public, max-age=31536000, immutable
+
+/data/techniques.json
+ Cache-Control: public, max-age=600, must-revalidate
diff --git a/src/components/SearchModal.astro b/src/components/SearchModal.astro
@@ -309,9 +309,16 @@ import Icon from './Icon.astro';
never in source Vite can scan, so it emits a genuine browser-native
import that fetches pagefind untouched. Pagefind's own internal chunk
imports then resolve against its real URL and are served straight from
- `public/pagefind/` (or `dist/pagefind/` in prod). */
- const nativeImport: (u: string) => Promise<any> =
- new Function('u', 'return import(u)') as any;
+ `public/pagefind/` (or `dist/pagefind/` in prod).
+
+ Dev only. `new Function` is an eval, and the site's Content Security
+ Policy (public/_headers) allows no eval, so in the build the import is
+ written plainly: Rollup leaves a runtime-string `import()` native, and
+ `import.meta.env.DEV` is a literal there, so the eval branch is not in
+ the bundle at all. */
+ const nativeImport: (u: string) => Promise<any> = import.meta.env.DEV
+ ? (new Function('u', 'return import(u)') as any)
+ : (u: string) => import(/* @vite-ignore */ u);
function loadPagefind() {
if (loading) return loading;