/** * Rate Limiting Middleware * * Implements IP-based and user-based rate limiting following OWASP best practices. * Uses in-memory storage for development (consider Redis for production). * * Security Controls: * - IP-based rate limiting to prevent abuse * - Configurable thresholds per endpoint * - Graceful HTTP 429 (Too Many Requests) responses * - Automatic reset windows * * OWASP Compliance: * - Follows "Fail Securely" principle * - Implements "Defense in Depth" with multiple rate limit layers */ import { NextRequest, NextResponse } from 'next/server' /** * Rate limit configuration per endpoint * Thresholds are requests per window (in milliseconds) */ interface RateLimitConfig { /** Maximum requests allowed per window */ maxRequests: number /** Time window in milliseconds */ windowMs: number /** Optional: Custom message for rate limit exceeded */ message?: string } /** * Default rate limit configurations * Following OWASP recommendations for sensible defaults */ const DEFAULT_RATE_LIMITS: Record = { '/api/predict': { maxRequests: 20, // 20 requests per minute for prediction endpoint (resource-intensive) windowMs: 60 * 1000, // 1 minute window message: 'Too many prediction requests. Please wait before trying again.', }, // Add more endpoint-specific limits as needed default: { maxRequests: 100, // 100 requests per minute for general endpoints windowMs: 60 * 1000, // 1 minute window message: 'Too many requests. Please wait before trying again.', }, } /** * In-memory store for rate limit tracking * Key: `${ip}:${endpoint}` * Value: { count: number, resetAt: number } * * Note: For production, replace with Redis or similar distributed cache * to handle multiple server instances and prevent memory leaks. */ const rateLimitStore = new Map() /** * Cleanup interval to prevent memory leaks * Removes expired entries every 5 minutes */ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000 setInterval(() => { const now = Date.now() for (const [key, value] of Array.from(rateLimitStore.entries())) { if (value.resetAt < now) { rateLimitStore.delete(key) } } }, CLEANUP_INTERVAL_MS) /** * Get client IP address from request * Handles various proxy headers (X-Forwarded-For, X-Real-IP) * Falls back to direct connection IP * * Security: Validates IP format to prevent header injection */ function getClientIP(request: NextRequest): string { // Check X-Forwarded-For header (first IP in chain) const forwardedFor = request.headers.get('x-forwarded-for') if (forwardedFor) { // X-Forwarded-For can contain multiple IPs, take the first one const firstIP = forwardedFor.split(',')[0].trim() // Basic IP validation (IPv4 or IPv6) if (/^[\d.:a-fA-F]+$/.test(firstIP)) { return firstIP } } // Check X-Real-IP header const realIP = request.headers.get('x-real-ip') if (realIP && /^[\d.:a-fA-F]+$/.test(realIP)) { return realIP } // Fallback to connection IP (may be undefined in serverless environments) return request.ip || 'unknown' } /** * Rate limiting middleware * * @param request - Next.js request object * @param endpoint - API endpoint path (e.g., '/api/predict') * @returns NextResponse with 429 status if rate limited, null if allowed * * OWASP Compliance: * - Implements "Fail Securely" by defaulting to deny * - Uses "Least Privilege" with minimal necessary data storage */ export function rateLimit( request: NextRequest, endpoint: string ): NextResponse | null { const clientIP = getClientIP(request) const config = DEFAULT_RATE_LIMITS[endpoint] || DEFAULT_RATE_LIMITS.default const key = `${clientIP}:${endpoint}` const now = Date.now() // Get or initialize rate limit entry let entry = rateLimitStore.get(key) // If entry doesn't exist or window has expired, create new entry if (!entry || entry.resetAt < now) { entry = { count: 0, resetAt: now + config.windowMs, } rateLimitStore.set(key, entry) } // Increment request count entry.count++ // Check if rate limit exceeded if (entry.count > config.maxRequests) { // Calculate retry-after header (seconds until window resets) const retryAfter = Math.ceil((entry.resetAt - now) / 1000) // Return 429 Too Many Requests response return NextResponse.json( { error: config.message || 'Too many requests', retryAfter, }, { status: 429, headers: { 'Retry-After': retryAfter.toString(), 'X-RateLimit-Limit': config.maxRequests.toString(), 'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': new Date(entry.resetAt).toISOString(), }, } ) } // Request allowed - update store and return null rateLimitStore.set(key, entry) // Add rate limit headers to successful responses // (Note: These will be added by the calling code) return null } /** * Get rate limit headers for successful requests * Allows clients to track their rate limit status */ export function getRateLimitHeaders( request: NextRequest, endpoint: string ): Record { const clientIP = getClientIP(request) const config = DEFAULT_RATE_LIMITS[endpoint] || DEFAULT_RATE_LIMITS.default const key = `${clientIP}:${endpoint}` const entry = rateLimitStore.get(key) if (!entry) { return { 'X-RateLimit-Limit': config.maxRequests.toString(), 'X-RateLimit-Remaining': config.maxRequests.toString(), } } return { 'X-RateLimit-Limit': config.maxRequests.toString(), 'X-RateLimit-Remaining': Math.max(0, config.maxRequests - entry.count).toString(), 'X-RateLimit-Reset': new Date(entry.resetAt).toISOString(), } }