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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhY1QhDXGWfyhrxaJvfpNt
This commit is contained in:
parent
40442317bb
commit
60af2e1621
|
|
@ -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<string, number[]>();
|
||||
|
||||
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" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue