Skip to content

Backend security hardening: fail-closed configs, proxy, billing, XSS, CSRF #25

@pablopunk

Description

@pablopunk

Goal

Harden backend security across five fronts: fail-closed configs, proxy header hygiene, billing accuracy, admin XSS prevention, and CSRF protection.

Merged from

Why this matters

Five independently-discovered backend security gaps that together close the most critical backend attack surface: missing config should deny, not allow; headers leaked to upstream should be explicit; billing should never be zero on success; admin rendering should escape untrusted data; and cookie-authed mutations should verify same-origin.

To do

1. Fail closed on missing security config (was #25)

Files: backend/src/lib/cron-auth.ts, backend/src/lib/ratelimit.ts

  • Add isProduction() helper (checks VERCEL_ENV || NODE_ENV === 'production')
  • Cron auth: when CRON_SECRET unset in prod → deny (fail closed)
  • Rate limiting: when Redis unset in prod → deny with ratelimit_misconfigured log (fail closed)
  • Dev behavior unchanged (permissive when no env)
  • Tests: cron-auth.test.ts, ratelimit.test.ts

2. Proxy header allowlist (was #26)

Files: backend/src/lib/proxy.ts

  • Replace denylist loop in buildForwardHeaders with explicit FORWARD_ALLOWLIST
  • Only forward: content-type, accept, accept-encoding, user-agent, provider-specific beta headers
  • Strip: authorization, cookie, x-api-key, x-forwarded-*, cf-*, vercel-*, internal headers
  • Always set upstream auth header last
  • Tests: proxy.test.ts

3. Billing fail-closed on missing usage (was #27)

Files: backend/src/lib/proxy.ts

  • Add resolveBillableTokens() — on 2xx with zero output tokens, fall back to estimated input + minimum output
  • Thread estimatedInputTokens through BillContext
  • Log usage_missing_on_success as error for alerting
  • Error responses (non-2xx) stay zero-cost — do NOT change
  • Tests: proxy-billing.test.ts

4. Admin dashboard XSS (was #28)

Files: backend/src/pages/admin.astro

  • Add esc() HTML-escape helper to inline script
  • Wrap every API-string field in esc(): model names, providers, emails, audit metadata
  • Do NOT change table structure or behavior
  • Verify: pnpm -C backend build exits 0

5. CSRF same-origin protection (was #29)

Files: backend/src/lib/csrf.ts, multiple mutation routes

  • Create requireSameOrigin(request) — returns null (ok) or 403 Response
  • Apply guard to all cookie-authed mutations: signout, token CRUD, admin mutations
  • Remove export const GET = POST from signout (GET signout is CSRF-able)
  • PAT/bearer routes (/api/v1/**) out of scope
  • Tests: csrf.test.ts

Verified together

  • mise exec -- pnpm -C backend test — all pass
  • mise exec -- pnpm test — exit 0
  • mise exec -- pnpm -C backend build — exit 0
  • No files outside scope modified per sub-item

Metadata

Metadata

Assignees

No one assigned

    Labels

    improveSurfaced by the improve skillsecuritySecurity finding

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions