Skip to content

Commit 664a8a6

Browse files
committed
ggg
1 parent ad2447d commit 664a8a6

2 files changed

Lines changed: 263 additions & 9 deletions

File tree

middleware.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import {
55
createCsrfError,
66
requiresCsrfProtection,
77
} from './src/lib/csrf';
8+
import {
9+
checkSimpleRateLimit,
10+
createSimpleRateLimitError,
11+
addSimpleRateLimitHeaders,
12+
} from './src/lib/simple-rate-limit';
813

914
/**
1015
* Security Middleware
@@ -138,20 +143,26 @@ const SECURITY_HEADERS = {
138143
/**
139144
* Middleware function
140145
*
141-
* Applies security headers and CSRF protection to all routes.
142-
* Runs on every request before reaching route handlers.
146+
* Applies security protections to all routes:
147+
* - Rate limiting (100 req/min general, 10 req/min auth)
148+
* - CSRF protection for state-changing operations
149+
* - Security headers (CSP, HSTS, X-Frame-Options, etc.)
143150
*
144-
* **CSRF Protection**:
145-
* - Validates CSRF tokens for POST, PUT, PATCH, DELETE requests
146-
* - Exempts GET, HEAD, OPTIONS, NextAuth, and webhook routes
147-
* - Returns 403 Forbidden if token validation fails
151+
* Runs on every request before reaching route handlers.
148152
*/
149153
export async function middleware(request: NextRequest) {
150-
// 1. CSRF Protection: Validate token for state-changing operations
151154
const { method, url } = request;
152155
const { pathname } = new URL(url);
153156

154-
// Check if CSRF protection is required for this request
157+
// 1. Rate Limiting: Check request limits
158+
const rateLimitResult = checkSimpleRateLimit(request);
159+
160+
if (!rateLimitResult.success) {
161+
// Return 429 Too Many Requests
162+
return createSimpleRateLimitError(rateLimitResult);
163+
}
164+
165+
// 2. CSRF Protection: Validate token for state-changing operations
155166
if (requiresCsrfProtection(method, pathname)) {
156167
// Validate CSRF token (async operation)
157168
const isValid = await validateCsrfTokenFromRequest(request);
@@ -162,14 +173,17 @@ export async function middleware(request: NextRequest) {
162173
}
163174
}
164175

165-
// 2. Security Headers: Apply to all responses
176+
// 3. Security Headers: Apply to all responses
166177
const response = NextResponse.next();
167178

168179
// Apply all security headers
169180
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
170181
response.headers.set(key, value);
171182
});
172183

184+
// Add rate limit headers to response
185+
addSimpleRateLimitHeaders(response, rateLimitResult);
186+
173187
// Remove X-Powered-By header if present (extra safety)
174188
response.headers.delete('X-Powered-By');
175189

src/lib/simple-rate-limit.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// src/lib/simple-rate-limit.ts
2+
// Simple IP-based rate limiting for API routes
3+
// Used for general API protection (100 req/min) and auth endpoints (10 req/min)
4+
5+
/**
6+
* Rate limit configuration
7+
*/
8+
export const SIMPLE_RATE_LIMIT_CONFIG = {
9+
// General API endpoints: 100 requests per minute
10+
general: {
11+
limit: 100,
12+
windowMs: 60 * 1000, // 60 seconds
13+
},
14+
15+
// Authentication endpoints: 10 requests per minute (stricter)
16+
auth: {
17+
limit: 10,
18+
windowMs: 60 * 1000,
19+
},
20+
};
21+
22+
/**
23+
* In-memory rate limit store
24+
*/
25+
interface RateLimitEntry {
26+
count: number;
27+
resetTime: number;
28+
}
29+
30+
const rateLimitStore = new Map<string, RateLimitEntry>();
31+
32+
/**
33+
* Cleanup expired entries periodically
34+
*/
35+
if (typeof setInterval !== 'undefined') {
36+
setInterval(() => {
37+
const now = Date.now();
38+
for (const [key, entry] of rateLimitStore.entries()) {
39+
if (now > entry.resetTime) {
40+
rateLimitStore.delete(key);
41+
}
42+
}
43+
}, 60 * 1000); // Cleanup every 60 seconds
44+
}
45+
46+
/**
47+
* Get rate limit configuration for a given pathname
48+
*/
49+
export function getSimpleRateLimitConfig(pathname: string): {
50+
limit: number;
51+
windowMs: number;
52+
} {
53+
// Authentication endpoints: stricter limits
54+
if (pathname.startsWith('/api/auth/login') ||
55+
pathname.startsWith('/api/auth/register') ||
56+
pathname.startsWith('/api/auth/forgot-password')) {
57+
return SIMPLE_RATE_LIMIT_CONFIG.auth;
58+
}
59+
60+
// Default: general API limits
61+
return SIMPLE_RATE_LIMIT_CONFIG.general;
62+
}
63+
64+
/**
65+
* Check if a route should be rate limited
66+
*/
67+
export function shouldSimpleRateLimit(pathname: string): boolean {
68+
// Don't rate limit health check endpoints
69+
if (pathname === '/api/health' || pathname === '/api/status') {
70+
return false;
71+
}
72+
73+
// Don't rate limit static assets
74+
if (pathname.startsWith('/_next/') ||
75+
pathname.startsWith('/static/') ||
76+
pathname === '/favicon.ico' ||
77+
pathname === '/robots.txt' ||
78+
pathname === '/sitemap.xml') {
79+
return false;
80+
}
81+
82+
// Rate limit all API routes
83+
if (pathname.startsWith('/api/')) {
84+
return true;
85+
}
86+
87+
// Don't rate limit other routes (pages, etc.)
88+
return false;
89+
}
90+
91+
/**
92+
* Get client IP address from request
93+
*/
94+
export function getClientIp(request: Request): string {
95+
// Try to get real IP from headers (reverse proxy, CDN)
96+
const forwardedFor = request.headers.get('x-forwarded-for');
97+
if (forwardedFor) {
98+
// x-forwarded-for can be a comma-separated list; take the first IP
99+
return forwardedFor.split(',')[0].trim();
100+
}
101+
102+
const realIp = request.headers.get('x-real-ip');
103+
if (realIp) {
104+
return realIp;
105+
}
106+
107+
// Vercel-specific headers
108+
const vercelIp = request.headers.get('x-vercel-forwarded-for');
109+
if (vercelIp) {
110+
return vercelIp.split(',')[0].trim();
111+
}
112+
113+
// Fallback to a default (shouldn't happen in production)
114+
return '127.0.0.1';
115+
}
116+
117+
/**
118+
* Rate limit result
119+
*/
120+
export interface SimpleRateLimitResult {
121+
success: boolean;
122+
limit: number;
123+
remaining: number;
124+
reset: number; // Unix timestamp in seconds
125+
retryAfter?: number; // Seconds until reset
126+
}
127+
128+
/**
129+
* Check rate limit for a request
130+
*/
131+
export function checkSimpleRateLimit(
132+
request: Request
133+
): SimpleRateLimitResult {
134+
const { pathname } = new URL(request.url);
135+
136+
// Check if route should be rate limited
137+
if (!shouldSimpleRateLimit(pathname)) {
138+
return {
139+
success: true,
140+
limit: 0,
141+
remaining: 0,
142+
reset: 0,
143+
};
144+
}
145+
146+
// Get rate limit config for this route
147+
const config = getSimpleRateLimitConfig(pathname);
148+
149+
// Get client IP
150+
const ip = getClientIp(request);
151+
152+
// Create rate limit key: ip:pathname
153+
const key = `${ip}:${pathname}`;
154+
155+
// Get current time
156+
const now = Date.now();
157+
158+
// Get or create rate limit entry
159+
let entry = rateLimitStore.get(key);
160+
161+
if (!entry || now > entry.resetTime) {
162+
// Create new entry or reset expired entry
163+
entry = {
164+
count: 1,
165+
resetTime: now + config.windowMs,
166+
};
167+
rateLimitStore.set(key, entry);
168+
} else {
169+
// Increment count
170+
entry.count++;
171+
}
172+
173+
// Calculate reset timestamp (seconds)
174+
const reset = Math.ceil(entry.resetTime / 1000);
175+
176+
// Calculate remaining
177+
const remaining = Math.max(0, config.limit - entry.count);
178+
179+
// Check if limit exceeded
180+
if (entry.count > config.limit) {
181+
// Calculate retry after (seconds until window resets)
182+
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
183+
184+
return {
185+
success: false,
186+
limit: config.limit,
187+
remaining: 0,
188+
reset,
189+
retryAfter,
190+
};
191+
}
192+
193+
return {
194+
success: true,
195+
limit: config.limit,
196+
remaining,
197+
reset,
198+
};
199+
}
200+
201+
/**
202+
* Create rate limit error response
203+
*/
204+
export function createSimpleRateLimitError(result: SimpleRateLimitResult): Response {
205+
return new Response(
206+
JSON.stringify({
207+
error: {
208+
code: 'RATE_LIMIT_EXCEEDED',
209+
message: 'Too many requests. Please try again later.',
210+
timestamp: new Date().toISOString(),
211+
},
212+
}),
213+
{
214+
status: 429,
215+
headers: {
216+
'Content-Type': 'application/json',
217+
'Retry-After': result.retryAfter?.toString() || '60',
218+
'X-RateLimit-Limit': result.limit.toString(),
219+
'X-RateLimit-Remaining': result.remaining.toString(),
220+
'X-RateLimit-Reset': result.reset.toString(),
221+
},
222+
}
223+
);
224+
}
225+
226+
/**
227+
* Add rate limit headers to response
228+
*/
229+
export function addSimpleRateLimitHeaders(
230+
response: Response,
231+
result: SimpleRateLimitResult
232+
): void {
233+
if (result.limit === 0) {
234+
return; // No rate limiting applied
235+
}
236+
237+
response.headers.set('X-RateLimit-Limit', result.limit.toString());
238+
response.headers.set('X-RateLimit-Remaining', result.remaining.toString());
239+
response.headers.set('X-RateLimit-Reset', result.reset.toString());
240+
}

0 commit comments

Comments
 (0)