From 60af2e16210129217f92399c18bdb735c4b1b092 Mon Sep 17 00:00:00 2001 From: MBO-Tech-IT Date: Tue, 21 Jul 2026 14:12:50 +0200 Subject: [PATCH] fix: add IP rate limiting and guard non-string email in flyer route Adds an in-memory, IP-based fixed-window rate limit (max 3 req/hour) to app/api/familyguard-flyer/route.ts to mitigate an email-spam/abuse vector where any client could trigger outbound emails and DB inserts with no limit. Also guards against a non-string `email` field in the request body, which previously threw an uncaught TypeError (HTTP 500) instead of the intended 400 validation response. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LhY1QhDXGWfyhrxaJvfpNt --- app/api/familyguard-flyer/route.ts | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/app/api/familyguard-flyer/route.ts b/app/api/familyguard-flyer/route.ts index 9cc48fd..af767ec 100644 --- a/app/api/familyguard-flyer/route.ts +++ b/app/api/familyguard-flyer/route.ts @@ -4,11 +4,42 @@ import { createServiceClient } from "@/lib/supabase"; const FLYER_DOWNLOAD_URL = "/downloads/MBO_FamilyGuard_Flyer_01.pdf"; +const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; +const RATE_LIMIT_MAX = 3; +const requestLog = new Map(); + +function isRateLimited(ip: string): boolean { + const now = Date.now(); + const timestamps = (requestLog.get(ip) ?? []).filter( + (t) => now - t < RATE_LIMIT_WINDOW_MS + ); + if (timestamps.length >= RATE_LIMIT_MAX) { + requestLog.set(ip, timestamps); + return true; + } + timestamps.push(now); + requestLog.set(ip, timestamps); + return false; +} + +function getClientIp(request: Request): string { + const forwarded = request.headers.get("x-forwarded-for"); + if (forwarded) return forwarded.split(",")[0].trim(); + return "unknown"; +} + function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } export async function POST(request: Request) { + if (isRateLimited(getClientIp(request))) { + return NextResponse.json( + { ok: false, error: "Zu viele Anfragen. Bitte versuchen Sie es später erneut." }, + { status: 429 } + ); + } + let body: { email?: string; dsgvoEinwilligung?: boolean }; try { body = await request.json(); @@ -16,7 +47,7 @@ export async function POST(request: Request) { return NextResponse.json({ ok: false, error: "Ungültige Anfrage" }, { status: 400 }); } - const email = body.email?.trim() ?? ""; + const email = typeof body.email === "string" ? body.email.trim() : ""; if (!isValidEmail(email)) { return NextResponse.json( { ok: false, error: "Bitte geben Sie eine gültige E-Mail-Adresse ein" },