fix(forms): rate-limit anonymous submitForm (E2, P2)

Anonymous, captcha-less submitForm was an unbounded DB-bloat vector.
Added a generous per-IP+form limit (20/min) — deliberately loose
because legitimate public signups at a hackerspace commonly share one
NAT/wifi IP, so a tight limit would false-block a room; this still
bounds automated flooding. Captcha (the real control) stays the
documented deferred follow-up. 532 tests.
This commit is contained in:
Moheeb Zara
2026-05-19 02:11:42 -07:00
parent c2ba9586fa
commit f11f4b3771
+12
View File
@@ -12,6 +12,7 @@ import {
type Member,
type ServerSupabase,
} from '@/lib/auth-helpers'
import { checkRateLimit } from '@/lib/security'
import {
createFormSchema,
updateFormSchema,
@@ -446,6 +447,17 @@ export async function submitForm(input: unknown) {
return { error: 'This form is not accepting responses' }
}
// Anonymous endpoint with no captcha: bound the DB-bloat abuse vector.
// Generous per-IP+form window because legitimate public signups at a
// hackerspace often share one NAT/wifi IP (a captcha is the real control,
// intentionally deferred). Best-effort IP from the proxy header.
const h = await headers()
const ip = (h.get('x-forwarded-for')?.split(',')[0] ?? '').trim() || 'unknown'
const rl = checkRateLimit(`form-submit:${ip}:${form.id}`, 20, 60_000)
if (!rl.allowed) {
return { error: 'Too many submissions from your network. Please wait a moment and try again.' }
}
const supabase = await createClient()
const {
data: { user },