cropintel / lib /security /rateLimiter.ts
Jaithra Polavarapu
CropIntel — HF Space deploy (all-in-one app)
889dd1b
Raw
History Blame Contribute Delete
5.84 kB
/**
* 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<string, RateLimitConfig> = {
'/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<string, { count: number; resetAt: number }>()
/**
* 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<string, string> {
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(),
}
}