Skip to content

Commit 7bdf8c4

Browse files
fix(api): add IP rate limiting to track-user endpoint to prevent DoS (JhaSourav07#595)
* fix(svg): sanitize hex colors in monthly badge to prevent XSS * fix(github): cap max pages in fetchUserRepos to prevent DoS * fix(api): add IP rate limiting to track-user endpoint to prevent DoS
1 parent 0da1292 commit 7bdf8c4

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

app/api/track-user/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('POST /api/track-user', () => {
3636
it('returns 400 for malformed JSON request bodies', async () => {
3737
const malformedRequest = {
3838
json: vi.fn().mockRejectedValue(new SyntaxError('Unexpected token')),
39+
headers: new Headers(),
3940
} as unknown as Request;
4041

4142
const response = await POST(malformedRequest);

app/api/track-user/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import { NextResponse } from 'next/server';
22
import dbConnect from '@/lib/mongodb';
33
import { User } from '@/models/User';
4+
import { trackUserRateLimiter } from '@/lib/rate-limit';
5+
46
export async function POST(req: Request) {
7+
// Get IP for rate limiting
8+
const ip = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || 'unknown';
9+
10+
if (ip !== 'unknown' && !trackUserRateLimiter.check(ip)) {
11+
return NextResponse.json(
12+
{ success: false, error: 'Too many requests, please try again later.' },
13+
{ status: 429 }
14+
);
15+
}
16+
517
let body: unknown;
618

719
try {

lib/rate-limit.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { TTLCache } from './cache';
2+
3+
// In-memory rate limiter to prevent basic DoS/spam (Denial of Wallet).
4+
// Note: In a serverless environment, this resets per cold-start/instance,
5+
// but it is highly effective at stopping aggressive single-instance spikes.
6+
// For multi-instance strict syncing, a Redis store (Vercel KV/Upstash) should be used.
7+
export class RateLimiter {
8+
private cache: TTLCache<number>;
9+
private limit: number;
10+
private windowMs: number;
11+
12+
constructor(limit = 5, windowMs = 60000) {
13+
this.limit = limit;
14+
this.windowMs = windowMs;
15+
// Set max capacity to 10000 IPs to prevent memory leaks from the rate limiter itself
16+
this.cache = new TTLCache<number>(10000, windowMs);
17+
}
18+
19+
// Returns true if allowed, false if rate limited
20+
check(ip: string): boolean {
21+
const current = this.cache.get(ip) || 0;
22+
if (current >= this.limit) {
23+
return false;
24+
}
25+
// We increment the count and reset the TTL, behaving similarly to a sliding window timeout.
26+
this.cache.set(ip, current + 1, this.windowMs);
27+
return true;
28+
}
29+
}
30+
31+
// Global instance for track-user endpoint (5 requests per IP per minute)
32+
export const trackUserRateLimiter = new RateLimiter(5, 60000);

0 commit comments

Comments
 (0)