Implement a full app-layer security hardening pass for the Next.js API/auth flows with these decided choices:
- Use distributed rate limiting with Upstash Redis.
- Enforce strict Origin/Referer CSRF checks on mutating routes.
- Harden OAuth callback redirects with allowlist + fallback.
- Add stronger browser security headers (CSP + HSTS).
- Add upload file magic-byte validation for PPT/PPTX.
- Standardize throttle responses as
429JSON withRetry-After. - Keep DB SQL changes and service-role env naming unchanged for this pass (explicitly requested).
- In scope:
src/app/api/**route hardening.- Shared security utilities under
src/server/**andsrc/server/http/**. next.config.tsheaders hardening.- Client logout flow update to use POST instead of GET-triggered logout.
- Tests and docs for all behavior changes.
- Out of scope for this pass (accepted risks):
- No new Supabase SQL migrations for RLS/constraints.
- No change to current
NEXT_PUBLIC_SUPABASE_SERVICE_ROLEsupport.
- New environment variables:
UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKENFOUNDATHON_ALLOWED_REDIRECT_HOSTS(comma-separated hostnames, optional ports allowed)
- Route behavior changes:
- Mutating routes return
403for CSRF failures with JSON error payload. - Rate-limited routes return
429with JSON body:{ "error": "Too many requests. Please try again later.", "code": "RATE_LIMITED" }
429responses include:Retry-AfterX-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
/api/auth/logoutwill be POST-only (GET removed; framework returns 405 automatically).
- OAuth callback redirect policy:
x-forwarded-hostis used only if inFOUNDATHON_ALLOWED_REDIRECT_HOSTS.- Otherwise fallback order: request origin (dev) -> configured site URL/origin (prod fallback-safe).
- Add
src/server/security/client-ip.ts
- Resolve client IP from headers in order:
cf-connecting-ipx-real-ip- first IP in
x-forwarded-for - fallback
"unknown"
- Add
src/server/security/rate-limit.ts
- Initialize Upstash Redis +
@upstash/ratelimit. - Define named policies with explicit windows/limits:
auth_login_ip: 20 / 10 minauth_callback_ip: 60 / 10 minproblem_lock_ip: 40 / 10 minproblem_lock_user: 10 / 10 minregister_create_ip: 20 / 10 minregister_create_user: 5 / 10 minregister_modify_ip: 60 / 10 minregister_modify_user: 20 / 10 minpresentation_upload_ip: 10 / hourpresentation_upload_user: 5 / hour
- Provide a helper returning either
null(allowed) or a readyNextResponse429. - Fail-mode behavior:
- Development: fail-open if Redis unavailable.
- Production: fail-closed with
503JSON for protected endpoints.
- Add
src/server/security/csrf.ts
- Validate mutating requests (
POST/PATCH/DELETE) by requiring same-origin via:Originheader match ORRefererorigin match.
- Return standardized 403 JSON on failure.
- Extend
src/server/http/response.ts
- Add helper for throttled responses with required headers.
- Keep current no-store behavior.
/api/auth/loginGET
- Apply IP limiter before OAuth call.
/api/auth/callbackGET
- Apply IP limiter.
- Replace current host trust logic with allowlist check.
/api/auth/logoutPOST
- Keep sign-out logic.
- Remove GET handler.
- Add CSRF check on POST.
/api/problem-statements/lockPOST
- CSRF check first.
- IP limiter.
- After auth context, user limiter.
/api/registerPOST/DELETE
- CSRF check.
- IP limiter.
- After auth context, user limiter.
- Keep GET unmodified except optional read limiter (not required in this pass).
/api/register/[teamId]PATCH/DELETE
- CSRF check.
- IP limiter.
- After auth context, user limiter.
/api/register/[teamId]/presentationPOST
- CSRF check.
- IP limiter.
- After auth context, user limiter.
- Add magic-byte validation in
src/lib/presentation.ts(or new helper undersrc/server/security):
.pptmust match CFB signature:D0 CF 11 E0 A1 B1 1A E1..pptxmust match ZIP signature:50 4B 03 04(or valid ZIP variant supported by Office).
- Enforce this in
submitTeamPresentationbefore upload. - Keep existing extension + MIME + size checks as additional gates.
- Update
next.config.tsheaders:
- Add CSP with sources compatible with existing app behavior:
default-src 'self'script-src 'self'(dev may include'unsafe-eval'only)style-src 'self' 'unsafe-inline' https://fonts.googleapis.comfont-src 'self' https://fonts.gstatic.com data:img-src 'self' data: blob: https:connect-src 'self' https://*.supabase.coframe-src https://view.officeapps.live.comobject-src 'none'base-uri 'self'form-action 'self'frame-ancestors 'none'
- Add HSTS in production responses:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
- Keep existing headers.
- Update
HeaderClientlogout action:
- Replace
window.location.assign("/api/auth/logout")with POST form submission to/api/auth/logout. - Preserve current UX (disable while logging out, close menu first).
- Add structured security logs for:
- Rate-limit denies (
route,policy,keyType,ip, optionaluserId). - CSRF rejects (
route,ip, origin/referrer presence). - OAuth host allowlist rejects (
forwardedHost,origin).
- Use sanitized logs (no tokens, no raw PII beyond hashed/partial identifiers where needed).
rate-limithelper:
- allows under limit
- blocks over limit with correct headers/body
- dev fail-open when Redis missing
- prod fail-closed when Redis missing
csrfhelper:
- accepts matching origin
- accepts matching referer origin
- rejects cross-origin
- rejects missing both
- upload magic-byte validator:
- valid ppt
- valid pptx
- extension/signature mismatch rejected
/api/auth/login:
- returns 429 when limited
/api/auth/callback:
- uses allowlisted forwarded host
- ignores non-allowlisted forwarded host and falls back
/api/auth/logout:
- GET returns 405
- POST with invalid origin returns 403
- POST success still returns 303 redirect
/api/problem-statements/lock,/api/register,/api/register/[teamId],/presentation:
- CSRF reject case (403)
- rate-limit reject case (429)
- existing success paths still pass
bun run testfull suite.- Verify no API contract regressions in existing frontend flows:
- sign-in
- register
- lock statement
- upload presentation
- logout redirect.
- Update
.env.example:
- add Upstash vars
- add
FOUNDATHON_ALLOWED_REDIRECT_HOSTSexample
- Update README security section:
- rate-limit policies and fail modes
- CSRF requirement for mutating API calls
- redirect allowlist behavior
- note unresolved DB-level hardening due current scope decision.
- Chosen defaults from your decisions:
- Upstash REST for limiter backend.
- Strict Origin/Referer CSRF enforcement.
- OAuth host allowlist + fallback.
- 429 responses include
Retry-Afterand rate headers. - No SQL migration hardening this pass.
- No service-role env naming changes this pass.
- Operational assumption:
- Production will provide valid Upstash credentials before deploy; without them, protected routes return 503 (fail-closed in prod).