Skip to content

fix(security): Add security headers and harden external link rendering - #13

Open
euxaristia wants to merge 2 commits into
Gitlawb:mainfrom
euxaristia:fix/harden-external-links-and-headers
Open

fix(security): Add security headers and harden external link rendering#13
euxaristia wants to merge 2 commits into
Gitlawb:mainfrom
euxaristia:fix/harden-external-links-and-headers

Conversation

@euxaristia

Copy link
Copy Markdown

Summary

  • Add missing security headers (CSP, X-Frame-Options, HSTS, Referrer-Policy, Permissions-Policy) via vercel.json
  • Reject javascript:/other unsafe URL schemes before rendering peer/repo URLs as <a href>, since those values come from the gossip network and node API, not this app
  • Update dependencies within semver to patch known vulnerabilities (dompurify, react-router-dom, postcss, nanoid)

Changes

  • vercel.json: adds a headers block applying CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and HSTS to every route. script-src allows 'self' plus a sha256 hash for the one inline anti-FOUC theme script in index.html; style-src keeps 'unsafe-inline' for React's inline style attributes and Google Fonts.
  • index.html: comment noting that editing the inline theme script requires recomputing its CSP hash, or it silently stops running.
  • src/lib/api.ts: new isHttpUrl() guard.
  • src/components/peers/PeerList.tsx: peer.http_url (from GET /api/v1/peers, i.e. other nodes on the gossip network) now only renders as a link when it's http(s); otherwise falls back to plain text. React doesn't scheme-filter href the way the DOMPurify-based markdown pipeline does for links.
  • src/components/repo-detail/ClonePanel.tsx, src/pages/HomePage.tsx: same guard on clone_url before rendering the "clone over https" link.
  • bun.lock, package.json: bun update (semver-respecting). Notably dompurify 3.4.11 → 3.4.14 and react-router-dom 7.17.0 → 7.18.2, both of which had open advisories.

Not included: sharp (via @vercel/og, used for OG image generation) and undici (via jsdom, test-only) both need a breaking major-version bump to clear their advisories — out of scope for a semver update.

Test plan

  • bun run lint
  • bun run test — 158 passed
  • bun run build — typecheck + production build succeed
  • Confirmed the built dist/index.html's inline theme script still matches the CSP sha256- hash
  • bun audit — dompurify, react-router-dom, postcss, nanoid advisories cleared; sharp and undici remain (documented above)

vercel.json had no Content-Security-Policy, X-Frame-Options, HSTS,
Referrer-Policy, or Permissions-Policy, leaving the site framable and
CSP-less as a backstop against markdown/link injection. PeerList,
ClonePanel, and HomePage also rendered peer.http_url and clone_url
(both sourced from the gossip network / node API, not this app)
directly into <a href>, which React does not scheme-filter the way
DOMPurify does for the markdown pipeline — a javascript: URL from
a malicious peer would have executed on click.
bun update within existing semver ranges. Fixes advisories in dompurify
(XSS via detached-subtree hook removal, the markdown pipeline's
sanitizer), react-router-dom (open redirect, protocol-validation XSS,
inefficient-route-matching DoS), postcss, and nanoid.

Not fixed here: sharp (via @vercel/og, server-side OG image
generation) and undici (via jsdom, test-only) both need a breaking
major-version bump outside this update's semver ranges.
@euxaristia
euxaristia marked this pull request as ready for review August 19, 2026 13:15
@euxaristia

Copy link
Copy Markdown
Author

@coderabbitai full review

@Vasanthdev2004

Copy link
Copy Markdown
Contributor

The isHttpUrl guard is worth having — http_url and clone_url arrive from other nodes over gossip, and they were reaching <a href> directly with none of the DOMPurify filtering the markdown path gets. Nice catch.

One thing that supports merging, since it's the first objection I'd expect someone to raise: the CSP does not break syntax highlighting. script-src has no wasm-unsafe-eval, which would normally take Shiki out — but this repo deliberately runs Shiki's JavaScript regex engine rather than the WASM one (createJavaScriptRegexEngine in src/lib/highlight.ts, whose first line says "no WASM"), and no .wasm is emitted in the build. Checked against a production build rather than assumed.

Two maintenance footguns worth a comment in the code, because both fail silently and neither is obvious six months from now:

1. The sha256 pin and the inline theme script are now coupled. script-src allowlists that script by hash, so editing it — including whitespace — stops it executing under CSP with no error anywhere. The symptom is a theme flash on first paint and the stored preference being ignored, which reads as a theming bug rather than a CSP one. The note added to index.html covers it; it may be worth the same note beside the hash in vercel.json, since that is the side someone updating the script will not be looking at.

2. connect-src and FEDERATED_NODES have to move together. The policy allowlists the four node hosts explicitly, and src/lib/nodes.ts lists the same four. Adding a fifth node to the registry without updating the CSP means its fetches are blocked in production only — dev has no CSP, so it passes locally and fails after deploy. vercel.json already has to be updated for the proxy rewrite when a node is added, so the two edits at least live in the same file; a comment pointing at nodes.ts would make the dependency explicit.

Neither blocks merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants