Skip to content

Commit f370e04

Browse files
aster-voidclaude
andcommitted
modules/site: add rate limiting for redirect DB lookups
Prevent DoS attacks on legacy URL redirect DB lookups: - 10 requests per 60 seconds per IP - Returns 429 Too Many Requests when exceeded - Auto-cleanup of stale entries every 5 minutes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 33a43e1 commit f370e04

File tree

1 file changed

+44
-1
lines changed

1 file changed

+44
-1
lines changed

src/hooks.server.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Handle } from "@sveltejs/kit";
2-
import { redirect } from "@sveltejs/kit";
2+
import { error, redirect } from "@sveltejs/kit";
33
import { sequence } from "@sveltejs/kit/hooks";
44
import { svelteKitHandler } from "better-auth/svelte-kit";
55
import { like } from "drizzle-orm";
@@ -12,6 +12,42 @@ const handleAuth: Handle = async ({ event, resolve }) => {
1212
return await svelteKitHandler({ event, resolve, auth, building });
1313
};
1414

15+
// Rate limiter for DB lookup redirects (10 requests per 60 seconds per IP)
16+
const RATE_LIMIT_WINDOW_MS = 60_000;
17+
const RATE_LIMIT_MAX_REQUESTS = 10;
18+
const rateLimitMap = new Map<string, number[]>();
19+
20+
function isRateLimited(ip: string): boolean {
21+
const now = Date.now();
22+
const timestamps = rateLimitMap.get(ip) ?? [];
23+
const validTimestamps = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
24+
25+
if (validTimestamps.length >= RATE_LIMIT_MAX_REQUESTS) {
26+
rateLimitMap.set(ip, validTimestamps);
27+
return true;
28+
}
29+
30+
validTimestamps.push(now);
31+
rateLimitMap.set(ip, validTimestamps);
32+
return false;
33+
}
34+
35+
// Cleanup stale entries every 5 minutes
36+
setInterval(
37+
() => {
38+
const now = Date.now();
39+
for (const [ip, timestamps] of rateLimitMap) {
40+
const valid = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
41+
if (valid.length === 0) {
42+
rateLimitMap.delete(ip);
43+
} else {
44+
rateLimitMap.set(ip, valid);
45+
}
46+
}
47+
},
48+
5 * 60 * 1000,
49+
);
50+
1551
const handleRedirect: Handle = async ({ event, resolve }) => {
1652
const path = event.url.pathname;
1753

@@ -33,6 +69,13 @@ const handleRedirect: Handle = async ({ event, resolve }) => {
3369
if (/^\d{4}-\d{2}-\d{2}-/.test(oldSlug)) {
3470
return resolve(event);
3571
}
72+
73+
// Rate limit DB lookups
74+
const ip = event.getClientAddress();
75+
if (isRateLimited(ip)) {
76+
error(429, "Too many requests");
77+
}
78+
3679
// DB lookup: find article where slug ends with the old slug pattern
3780
const found = await db
3881
.select({ slug: article.slug })

0 commit comments

Comments
 (0)