From f11f4b3771e414daa128fa37261c587f1f82dd5f Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Tue, 19 May 2026 02:11:42 -0700 Subject: [PATCH] fix(forms): rate-limit anonymous submitForm (E2, P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/actions/forms.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/actions/forms.ts b/lib/actions/forms.ts index d1652fe..f4f272c 100644 --- a/lib/actions/forms.ts +++ b/lib/actions/forms.ts @@ -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 },