Web security hardening: headers/CSP, URL scheme guard, tests - #6
Merged
Conversation
Adds public/_headers with X-Frame-Options: DENY, a CSP frame-ancestors 'none', X-Content-Type-Options: nosniff, Referrer-Policy, base-uri 'self' and object-src 'none'. These are behaviour-neutral (they do not govern script/style/img/connect loading). The resource-directive CSP that would backstop XSS is drafted in the file, disabled pending build-hash finalisation and in-app review (a tight script-src breaks Mantine's inline ColorSchemeScript and the React Router bootstrap). Addresses security review 2026-07-14 finding consfyi#3.
Data-sourced URLs (a convention's url, a key-date source post) from data.cons.fyi were passed straight to Anchor href. React does not block javascript: URLs, so a poisoned value would be a script-execution sink on click. safeExternalUrl() passes through only http(s) URLs; anything else yields undefined, leaving the anchor inert. Applied at the four data-sourced sinks (EventRow website link, EventDetails source post + official-site links). Fixed-scheme bsky.app/github.com template hrefs are unaffected (React attribute-escaping keeps them inert). Addresses security review 2026-07-14 finding consfyi#2.
Cloudflare Pages serves static files, so a per-request nonce is impossible and 3 of the 5 inline bootstrap scripts embed per-build content hashes. scripts/inject-csp.mjs (postbuild hook) computes the sha256 of every inline script in the built index.html and rewrites build/client/_headers with the full policy: script-src 'self' <hashes> plus scoped style/img/connect/font/ worker directives. javascript: URLs match neither 'self' nor any hash, so the CSP now backstops the anchor sinks safeExternalUrl already guards. The committed public/_headers keeps only the safe, behaviour-neutral tier, so a build that skips postbuild falls back to a valid CSP, never a broken one. Verified under wrangler pages dev: zero CSP violations across the list, con detail, and map (Mantine theme, React Router bootstrap, and maplibre/protomaps all load clean). Addresses security review 2026-07-14 finding consfyi#3 (full CSP).
The href scheme guard from the previous commit had no regression test and the repo had no test runner. Adds vitest + a unit test covering the security-critical branches (javascript:/data:/vbscript: and whitespace/case variants blocked, http(s) passed through, relative/empty/nullish fail closed), a standalone vitest.config.ts (so tests don't load the app build plugins), and a ci.yml running typecheck + test on pull_request so the guard's regressions are caught in CI. Follow-up to security review 2026-07-14 finding consfyi#2.
Keeps review screenshots and wrangler pages dev state out of the tree.
The postbuild step now emits the full strict policy (script-src hashes + scoped directives) as Content-Security-Policy-Report-Only instead of enforcing it, and keeps only the behaviour-neutral safe tier (frame-ancestors/base-uri/object-src) as the enforced Content-Security-Policy. Report-only cannot block anything, so this is safe on untested routes (the OAuth login + authed actions were not driven under CSP). Promote the strict policy to the enforced header once it runs clean against real traffic. Refines security review 2026-07-14 finding consfyi#3.
sparkyfen
force-pushed
the
security/web-hardening
branch
from
July 17, 2026 04:42
e239e49 to
a57b9f6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security hardening for the web client (from an internal review).
public/_headers):X-Frame-Options: DENY, an enforced CSPframe-ancestors 'none'+base-uri/object-src,nosniff,Referrer-Policy— clickjacking + hardening, behaviour-neutral.safeExternalUrl): data-sourced anchorhrefs pass through an http(s)-only guard, so a non-web-scheme value can't become a clickable sink. Applied at the four data-sourced sinks; fixed-scheme bsky/github links unaffected. Unit-tested.postbuildstep pinsscript-srcto the sha256 of each inline bootstrap script plus scoped resource directives, emitted asContent-Security-Policy-Report-Only. So the strict policy is observed (not enforced) and cannot break the app; the enforced header stays the safe tier. Verified underwrangler pages devwith zero violations across list/detail/map. Violations surface in the browser console; promote to enforced once it runs clean against real traffic incl. the OAuth login.ciworkflow on pull_request.Verified:
pnpm typecheck,pnpm test(3 passing), production build + CSP drive.