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 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 {
|
function isValidEmail(email: string): boolean {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
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 };
|
let body: { email?: string; dsgvoEinwilligung?: boolean };
|
||||||
try {
|
try {
|
||||||
body = await request.json();
|
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 });
|
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)) {
|
if (!isValidEmail(email)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ ok: false, error: "Bitte geben Sie eine gültige E-Mail-Adresse ein" },
|
{ ok: false, error: "Bitte geben Sie eine gültige E-Mail-Adresse ein" },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue