Skip to content

harden: add security headers and protect the geocode proxy#87

Open
Yali444 wants to merge 1 commit into
mainfrom
claude/senior-dev-code-review-ubhi3j
Open

harden: add security headers and protect the geocode proxy#87
Yali444 wants to merge 1 commit into
mainfrom
claude/senior-dev-code-review-ubhi3j

Conversation

@Yali444

@Yali444 Yali444 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

Two production-hardening changes from a senior-dev review. The codebase is in good shape (near-zero any, pure tested functions, logic in hooks, thoughtful cache headers), so this is not a cleanup/rewrite — it targets the two things with real risk behind them. No UI behavior changes.

1. Security headers (next.config.ts)

The site set cache headers but no security headers at all. Added to every route:

  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • X-Frame-Options: SAMEORIGIN
  • Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  • Permissions-Policy: camera=(), microphone=(), payment=(), geolocation=(self) — geolocation kept enabled because the app uses useGeolocation.
  • Content-Security-Policy shipped as Report-Only first.

CSP is Report-Only intentionally: the app relies on inline styles/scripts (framer-motion, next-themes theme script, Vercel Analytics, Disqus) and several external origins (Unsplash, OSM tiles, Supabase). An enforced strict policy would risk white-screening the site, so this lets us validate against real traffic before flipping it on. The Supabase origin in connect-src is derived from NEXT_PUBLIC_SUPABASE_URL, not hardcoded.

2. Geocode proxy hardening (src/app/api/geocode/route.ts)

The endpoint proxied OpenStreetMap Nominatim with cache: "no-store", no rate limit, and no input bounds — effectively an open proxy that re-fetched on every keystroke, violated Nominatim's ≤1 req/sec usage policy (risking an IP/User-Agent ban that breaks address search for everyone), and could be hammered by anyone. Now it:

  • Caches upstream results (next: { revalidate: 86400 } + edge Cache-Control: public, s-maxage=86400, stale-while-revalidate) — the biggest win for ToS compliance and latency.
  • Caps query length (400 on q > 200 chars, before fetching).
  • Applies a lightweight per-IP rate limit (30/min → 429). It's a module-scoped in-memory limiter; a comment notes it's per-instance, not distributed, and a durable store (e.g. Upstash) is the upgrade path if real abuse appears.

All existing response contracts are preserved (400 on missing/whitespace q, status propagation on non-OK upstream, 502 on throw, {result:null} shapes). The consumer (useAddressSearch) already surfaces non-OK responses as a benign search error, so no client change was needed.

Verification

  • npm run test266 tests pass (added 5 covering the new geocode paths: length cap, Cache-Control header, per-IP rate limit + 429, per-IP isolation)
  • npm run lint — clean
  • npm run build — succeeds
  • ✅ Confirmed via next start + curl that all six headers emit on / and the global headers reach /api/*

Follow-ups (not in this PR)

  • Promote CSP from Report-Only → enforcing once violation reports are clean, ideally moving script-src to a nonce instead of 'unsafe-inline'.
  • Durable/shared rate limiter if the endpoint sees real abuse.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Wos5Hzyyjw79uzN1w1T42


Generated by Claude Code

Two production-hardening changes; no behavioral change to the UI.

next.config.ts: add baseline security headers to every route
(X-Content-Type-Options, Referrer-Policy, X-Frame-Options, HSTS,
Permissions-Policy) plus a Content-Security-Policy in Report-Only mode.
CSP ships Report-Only first because the app relies on inline styles/scripts
(framer-motion, next-themes, analytics, Disqus) and several external origins;
this lets us validate the policy before enforcing it. The Supabase origin in
connect-src is derived from NEXT_PUBLIC_SUPABASE_URL. geolocation stays
enabled since the app uses it.

src/app/api/geocode: the proxy used cache:"no-store" with no rate limit or
input bounds, making it an open proxy that hammered Nominatim and risked an
IP ban (Nominatim allows <=1 req/sec). Now caches upstream results
(revalidate 86400 + edge Cache-Control), caps query length, and applies a
lightweight per-IP rate limit. Existing response contracts are preserved;
tests extended to cover the new paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wos5Hzyyjw79uzN1w1T42
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ca-fe Ready Ready Preview, Comment Jun 19, 2026 7:18am

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