Security & robustness hardening pass

Cross-cutting input-validation, isolation, and DoS-resistance fixes across
the app, API, billing, queue, and infra layers.

- Runtime validation (zod) for client-supplied admin actions (role/plan/
  limits), series generation index, and all pg-boss queue payloads
- Auth: require email verification before sign-in; reject weak/placeholder/
  short BETTER_AUTH_SECRET in production
- Billing: sanitize Stripe/PayPal errors (log server-side, generic to client);
  race-safe subscription upsert; only count "processed" webhook events as
  handled; verify org membership in getEffectivePlan to block plan escalation
- Series generation: reserve usage up front and refund on failure; bill the
  owning org, not the caller's active org
- Injection defenses: HTML-escape user fields in emails, strip CR/LF from
  subject/recipient, validate ElevenLabs voiceId before URL interpolation
- Media routes: stream off disk instead of buffering whole files; rate-limit
  anonymous public audio/cover endpoints by client IP
This commit is contained in:
Leon Serfaty
2026-06-20 20:59:03 -04:00
parent cd1d6a1a28
commit 51c541ad22
21 changed files with 489 additions and 152 deletions
+3 -16
View File
@@ -19,22 +19,10 @@ const nextConfig = {
// Server-only packages that should be required at runtime, not bundled by webpack.
// better-auth ships internal adapters (kysely) that break webpack's ESM analysis.
serverExternalPackages: ["pg-boss", "@prisma/client", "better-auth"],
// Security headers applied to every route.
// Static security headers applied to every route. The Content-Security-Policy is
// intentionally NOT set here — it is built per-request with a nonce in middleware.ts
// so script-src can drop 'unsafe-inline'/'unsafe-eval' (nonce + 'strict-dynamic').
async headers() {
// Pragmatic CSP: Next.js's inline/runtime bootstrap currently requires
// 'unsafe-inline'/'unsafe-eval' in script-src. Future improvement: tighten
// to per-request nonces (and drop 'unsafe-*') once the app is migrated.
const csp = [
"default-src 'self'",
"img-src 'self' data: https://*.blob.core.windows.net https://images.unsplash.com",
"media-src 'self'",
"style-src 'self' 'unsafe-inline'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; ");
return [
{
source: "/:path*",
@@ -48,7 +36,6 @@ const nextConfig = {
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ key: "Content-Security-Policy", value: csp },
],
},
];