From 8b5c00f3aab523fb8a8c7e7599cbd142bbbe6ab3 Mon Sep 17 00:00:00 2001 From: dpdev69 <4314475+dpdev69@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:56:12 +0300 Subject: [PATCH] Site: strict CSP with per-request nonce, HSTS, X-Frame-Options, noindex on pages.dev Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 6 ++++++ functions/_middleware.js | 14 ++++++++++++++ site/_headers | 16 ++++++++++++++++ site/_routes.json | 5 +++++ 4 files changed, 41 insertions(+) create mode 100644 functions/_middleware.js create mode 100644 site/_headers create mode 100644 site/_routes.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a88dde5..d6f4ff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to Upright are listed here. The format follows ## [Unreleased] +### Changed + +- The website now sends a strict Content-Security-Policy, HSTS and X-Frame-Options on every + page, with a fresh nonce per page so Cloudflare's bot check can run, and the pages.dev preview + host is marked noindex. + ### Added - An [Upright vs OpenLogi](https://upright.hellobasalt.dev/upright-vs-openlogi) comparison page diff --git a/functions/_middleware.js b/functions/_middleware.js new file mode 100644 index 0000000..57454f0 --- /dev/null +++ b/functions/_middleware.js @@ -0,0 +1,14 @@ +// Gives each HTML response a fresh CSP nonce. Cloudflare's Bot Fight Mode injects an +// inline script into every page on the zone and copies the nonce from script-src onto +// it, so the script runs without 'unsafe-inline'. Our own scripts load from /assets/, +// and the one-line inline "js" class script is allowed by its hash in _headers. +export async function onRequest({ next }) { + const res = await next(); + const csp = res.headers.get("Content-Security-Policy"); + if (!csp || !(res.headers.get("Content-Type") || "").includes("text/html")) return res; + const bytes = crypto.getRandomValues(new Uint8Array(16)); + const nonce = btoa(String.fromCharCode(...bytes)); + const out = new Response(res.body, res); + out.headers.set("Content-Security-Policy", csp.replace("script-src 'self'", `script-src 'self' 'nonce-${nonce}'`)); + return out; +} diff --git a/site/_headers b/site/_headers new file mode 100644 index 0000000..b7aa75c --- /dev/null +++ b/site/_headers @@ -0,0 +1,16 @@ +/* + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + X-Frame-Options: DENY + Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() + Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'sha256-/x7W7R75k8Roq0WaVRQX9blP4OufE5xbAdzklGxsgpw='; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors 'none' + Strict-Transport-Security: max-age=31536000; includeSubDomains + +https://:project.pages.dev/* + X-Robots-Tag: noindex + +https://:version.:project.pages.dev/* + X-Robots-Tag: noindex + +/assets/* + Cache-Control: public, max-age=3600 diff --git a/site/_routes.json b/site/_routes.json new file mode 100644 index 0000000..8860b10 --- /dev/null +++ b/site/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/*"], + "exclude": ["/assets/*", "/robots.txt", "/sitemap.xml", "/llms.txt"] +}