From 8e6784ed0093d9734d7a5130a5c3ba5f427708dc Mon Sep 17 00:00:00 2001 From: K <39208150+Kvrnn@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:04:34 -0700 Subject: [PATCH] chore(security): remove Decap CMS and add baseline security headers - Remove public/decapcms/. It was publicly served at /decapcms/, loaded Netlify Identity and an unpinned decap-cms build from unpkg, and could not work on the template's Cloudflare deploy (git-gateway needs Netlify Identity). Its config also wrote to src/content/post (the post loader reads src/data/post), had no draft field, and committed straight to main. - Add X-Content-Type-Options, X-Frame-Options (SAMEORIGIN), and Referrer-Policy to public/_headers for prerendered pages and assets, and in middleware for on-demand responses (including /redirect). Permissions-Policy is intentionally omitted: IframeEmbed grants camera/microphone/geolocation to embedded CRM forms. --- public/_headers | 7 ++++++- public/decapcms/config.yml | 29 ----------------------------- public/decapcms/index.html | 14 -------------- src/middleware.ts | 27 +++++++++++++++++++++++++-- 4 files changed, 31 insertions(+), 46 deletions(-) delete mode 100644 public/decapcms/config.yml delete mode 100644 public/decapcms/index.html diff --git a/public/_headers b/public/_headers index 806338c1..843cb963 100644 --- a/public/_headers +++ b/public/_headers @@ -1,2 +1,7 @@ +/* + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + Referrer-Policy: strict-origin-when-cross-origin + /_astro/* - Cache-Control: public, max-age=31536000, immutable \ No newline at end of file + Cache-Control: public, max-age=31536000, immutable diff --git a/public/decapcms/config.yml b/public/decapcms/config.yml deleted file mode 100644 index 841a67b0..00000000 --- a/public/decapcms/config.yml +++ /dev/null @@ -1,29 +0,0 @@ -backend: - name: git-gateway - branch: main - -media_folder: 'src/assets/images' -public_folder: '/_astro' - -collections: - - name: 'post' - label: 'Post' - folder: 'src/content/post' - create: true - fields: - - { label: 'Title', name: 'title', widget: 'string' } - - { label: 'Excerpt', name: 'excerpt', widget: 'string' } - - { label: 'Category', name: 'category', widget: 'string' } - - { - label: 'Tags', - name: 'tags', - widget: 'list', - allow_add: true, - allow_delete: true, - collapsed: false, - field: { label: 'Tag', name: 'tag', widget: 'string' }, - } - - { label: 'Image', name: 'image', widget: 'string' } - - { label: 'Publish Date', name: 'publishDate', widget: 'datetime', required: false } - - { label: 'Author', name: 'author', widget: 'string' } - - { label: 'Content', name: 'body', widget: 'markdown' } diff --git a/public/decapcms/index.html b/public/decapcms/index.html deleted file mode 100644 index baab0e45..00000000 --- a/public/decapcms/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Content Manager - - - - - - - diff --git a/src/middleware.ts b/src/middleware.ts index 54bb2fb8..e3e7f422 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -27,7 +27,7 @@ export const onRequest = defineMiddleware(async (context, next) => { if (import.meta.env.DEV) { console.log('[Redirect Middleware] Skipping redirect page:', pathname); } - return next(); + return withSecurityHeaders(await next()); } // Debug logging (remove in production if desired) @@ -120,5 +120,28 @@ export const onRequest = defineMiddleware(async (context, next) => { } // No redirect found, continue with normal request handling - return next(); + return withSecurityHeaders(await next()); }); + +/** + * Baseline security headers for on-demand (Worker-rendered) responses. + * Prerendered pages and static assets get the same set from public/_headers. + * Keep the two lists in sync. + */ +const SECURITY_HEADERS: Record = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'SAMEORIGIN', + 'Referrer-Policy': 'strict-origin-when-cross-origin', +}; + +function withSecurityHeaders(response: Response): Response { + try { + for (const [name, value] of Object.entries(SECURITY_HEADERS)) response.headers.set(name, value); + return response; + } catch { + // Some responses have immutable headers; copy into a mutable response instead + const copy = new Response(response.body, response); + for (const [name, value] of Object.entries(SECURITY_HEADERS)) copy.headers.set(name, value); + return copy; + } +}