Every other public write route in the app goes through consumeSharedRateLimit from lib/rate-limit/shared.ts:
app/api/posts/route.ts
app/api/posts/[id]/vote/route.ts
app/api/comments/route.ts
app/api/upload/presign/route.ts
app/api/teams/waitlist/route.ts
app/api/newsletter/route.ts does not. It validates the email with zod and goes straight to the database and to sendNewsletterWelcome.
That makes it the cheapest endpoint to abuse in the app, and the most expensive one to have abused:
- a script can enqueue unlimited welcome emails to arbitrary addresses, which is an email-bomb primitive pointed at third parties from our domain
- every request costs a Resend send plus a Supabase write
- bulk sends to addresses that never asked for them is the fastest way to damage sending reputation, and that is hard to recover
This matters more now that traffic is climbing.
Suggested fix
Apply consumeSharedRateLimit the same way teams/waitlist/route.ts does, keyed by IP. Keep the limit tight, since a real person signs up once.
Worth checking separately whether a repeat signup for an address that is already subscribed re-sends the welcome email, because that is a second amplification path that a rate limit alone does not close.
Acceptance
Every other public write route in the app goes through
consumeSharedRateLimitfromlib/rate-limit/shared.ts:app/api/posts/route.tsapp/api/posts/[id]/vote/route.tsapp/api/comments/route.tsapp/api/upload/presign/route.tsapp/api/teams/waitlist/route.tsapp/api/newsletter/route.tsdoes not. It validates the email with zod and goes straight to the database and tosendNewsletterWelcome.That makes it the cheapest endpoint to abuse in the app, and the most expensive one to have abused:
This matters more now that traffic is climbing.
Suggested fix
Apply
consumeSharedRateLimitthe same wayteams/waitlist/route.tsdoes, keyed by IP. Keep the limit tight, since a real person signs up once.Worth checking separately whether a repeat signup for an address that is already
subscribedre-sends the welcome email, because that is a second amplification path that a rate limit alone does not close.Acceptance
POST /api/newsletteris rate limited per IPteams/waitlist/route.test.ts