This vulnerability occurs when an application fails to limit the number or frequency of requests a user (or IP address) can make to sensitive endpoints within a given time window. Without rate limiting, attackers can perform automated attacks like:
Brute-forcing credentials: Rapidly guessing passwords on a login page.
Rate limiting is typically implemented using middleware or dedicated libraries that track request counts per user ID, IP address, or API key over time (e.g., X requests per minute). Frameworks often have plugins or built-in support for this.Key Remediation Principles:
Identify Sensitive Endpoints: Apply rate limiting primarily to login attempts, password reset requests, token validations, expensive API calls, and form submissions.
Choose Appropriate Limits: Set reasonable limits based on expected legitimate usage (e.g., 5-10 login attempts per minute per IP/user).
Track by IP and/or User ID: Track anonymous requests (like login attempts) by IP address. Track authenticated requests by user ID and potentially IP address for better protection.
Implement Exponential Backoff (Optional but Recommended): Increase the delay for subsequent attempts after a limit is reached.
Logging and Monitoring: Log rate limit events to detect attacks.
Vulnerable Scenario 1: No Login Attempt Limit (Django)
# accounts/views.py# Standard Django LoginView without rate limiting appliedclass UserLoginView(LoginView): template_name = 'accounts/login.html' # DANGEROUS: No mechanism to prevent an attacker from submitting # thousands of password guesses per minute for a given username.
Vulnerable Scenario 2: Unprotected API Endpoint (Flask)
# app.py (Flask)@app.route('/api/search', methods=['GET'])def api_search(): query = request.args.get('q') # DANGEROUS: This search might be resource-intensive. # An attacker can flood this endpoint, causing DoS. # No rate limiting is applied. results = perform_complex_search(query) return jsonify(results)
Integrate a rate-limiting library. Apply decorators or middleware to the specific views/routes that need protection. Use Redis or Memcached for distributed rate limiting in multi-server environments.
# views.py (Django with django-ratelimit)from ratelimit.decorators import ratelimitclass UserLoginViewSecure(LoginView): template_name = 'accounts/login.html' # SECURE: Apply rate limiting decorator. # block=True returns 429 Too Many Requests when limit is exceeded. # key='ip' tracks by IP. key='user_or_ip' tracks by user if logged in, else IP. # key='post:username' tracks based on the 'username' field in the POST data + IP. @method_decorator(ratelimit(key='post:username', rate='5/m', block=True), name='dispatch') def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs)# app.py (Flask with Flask-Limiter)from flask_limiter import Limiterfrom flask_limiter.util import get_remote_addresslimiter = Limiter( get_remote_address, # Use IP address for tracking app=app, default_limits=["200 per day", "50 per hour"], storage_uri="memory://" # Use "redis://localhost:6379" etc. for production)@app.route('/api/search-secure')@limiter.limit("10 per minute") # SECURE: Specific limit for this endpointdef api_search_secure(): query = request.args.get('q') results = perform_complex_search(query) return jsonify(results)
Use automated tools (like wfuzz, ffuf, Burp Intruder) or simple scripts (e.g., curl in a loop) to send rapid, repeated requests to login endpoints, password reset forms, and resource-intensive API endpoints. Verify that after exceeding the configured limit, the server responds with an appropriate error (e.g., 429 Too Many Requests) and blocks further requests for a period. Test different keys (IP vs. user vs. form field).
Using libraries like Bucket4j, Resilience4j, Spring Cloud Gateway’s RequestRateLimiter filter, or custom implementations with caching (e.g., Guava Cache, Caffeine, Redis).
A REST controller exposes an endpoint without any rate limiting applied.
// controller/DataController.java@RestControllerpublic class DataController { @GetMapping("/api/public-data/{id}") public DataObject getPublicData(@PathVariable String id) { // DANGEROUS: This endpoint can be called rapidly by anyone. // If fetching data is expensive, this is a DoS vector. return dataService.fetchPublicData(id); }}
Integrate a rate-limiting library like Bucket4j via filters or aspects. Apply limits based on IP address for anonymous endpoints and user ID (from Authentication principal) for authenticated endpoints.
Use automated tools or scripts to send rapid requests to login endpoints and APIs. Verify that a 429 Too Many Requests response is received after exceeding the limit. Check for X-Rate-Limit-* headers if implemented. Test tracking by IP and, if applicable, by authenticated user ID.
Vulnerable Scenario 2: Expensive API Call Unprotected
An API endpoint performs a complex calculation but lacks specific rate limits.
// Controllers/CalculationController.cs[ApiController][Route("api/[controller]")]public class CalculationController : ControllerBase{ [HttpGet("complex")] public async Task<IActionResult> GetComplexCalculation(int input) { // DANGEROUS: This might take significant CPU time. // No rate limit applied, vulnerable to DoS. var result = await PerformExpensiveCalculation(input); return Ok(result); }}
Install and configure AspNetCoreRateLimit. Define rules in appsettings.json based on IP, client ID header, or specific endpoints. Apply the middleware globally or selectively. Use IDistributedCache with Redis for multi-server deployments.
Use automated tools or scripts to flood endpoints defined in the AspNetCoreRateLimit rules (e.g., login, specific APIs). Verify that 429 Too Many Requests responses are received after exceeding the limit. Check Retry-After headers. Test tracking by IP and client ID headers if configured.
Vulnerable Scenario 2: API Route Missing Throttle Middleware
A developer creates an API route but forgets to apply the throttle middleware.
// routes/api.phpuse App\Http\Controllers\DataController;// DANGEROUS: This route has no rate limit.Route::get('/data/{id}', [DataController::class, 'show']);// This route group is protectedRoute::middleware('auth:sanctum', 'throttle:60,1')->group(function () { Route::get('/user', function (Request $request) { return $request->user(); });});
Apply the throttle middleware to relevant routes or route groups in routes/web.php or routes/api.php. Configure different throttles (e.g., throttle:10,1 for 10 attempts per minute). Modern Laravel applies throttling to login attempts by default.
// routes/web.php (Modern Laravel - login throttled by default)Auth::routes(); // Includes throttling for login attempts// routes/api.php (Secure)use App\Http\Controllers\DataController;// SECURE: Apply throttle middleware to specific routes or groups.// Example: 100 requests per minute per authenticated user (using api guard)Route::middleware('throttle:100,1')->group(function () { Route::get('/data/{id}', [DataController::class, 'show']);});// Example: Stricter limit for sensitive action, by IPRoute::post('/sensitive-action', [ActionController::class, 'store']) ->middleware('throttle:5,1'); // 5 attempts per minute per IP// Authenticated routes group (throttled by user)Route::middleware('auth:sanctum', 'throttle:60,1')->group(function () { Route::get('/user', function (Request $request) { return $request->user(); });});
// app/Http/Kernel.php (Ensure throttle middleware is defined)protected $routeMiddleware = [ // ... other middleware 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, // Or named throttles: // 'throttle:basic' => \Illuminate\Routing\Middleware\ThrottleRequests::class . ':60,1',];
Use automated tools or scripts to send rapid requests to login routes and API endpoints. Verify that 429 Too Many Requests responses are received after the limit (e.g., after 5 login attempts in a minute). Check Retry-After and X-RateLimit-* headers in the response.
Vulnerable Scenario 2: Rate Limiter Applied After Routes
The rate limiting middleware is added too late in the middleware chain.
// app.jsconst rateLimit = require('express-rate-limit');// ... setup app ...app.post('/login', (req, res) => { /* ... */ }); // Defined BEFORE limiter// DANGEROUS: Limiter is applied after the /login route.// It will not affect the /login route.const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit each IP to 100 requests per windowMs});app.use('/api/', apiLimiter); // Only applies to routes starting with /api/app.get('/api/data', (req, res) => { res.json({ data: 'some data' }); });
Apply express-rate-limit middleware early in your middleware stack, either globally or specifically to sensitive routes/routers. Configure appropriate windowMs and max values. Use a keyGenerator based on IP or user ID. For distributed systems, use an external store like rate-limit-redis.
Use automated tools or scripts to send rapid requests to endpoints protected by express-rate-limit. Verify that 429 Too Many Requests responses are received after the max limit within the windowMs is reached. Check the response body for the configured message. Test different keys if custom keyGenerator is used.
A standard Rails application without the rack-attack gem installed or configured in config/initializers/rack_attack.rb.
# Gemfile (Missing rack-attack)# gem 'rack-attack' # This line is absent# config/application.rb (Middleware not inserted)# config.middleware.use Rack::Attack # This line is absent
Vulnerable Scenario 2: Expensive Action Not Throttled
rack-attack is configured for logins, but a resource-intensive search endpoint is left unprotected.
# config/initializers/rack_attack.rbRack::Attack.throttle('logins/ip', limit: 5, period: 60.seconds) do |req| req.ip if req.path == '/users/sign_in' && req.post?end# DANGEROUS: No throttle defined for the /search endpoint.# app/controllers/search_controller.rbdef index # Assumes perform_expensive_search is resource intensive @results = perform_expensive_search(params[:query]) # ... render ...end
Install and configure rack-attack. Define throttles in config/initializers/rack_attack.rb for sensitive paths like logins (/users/sign_in), password resets (/users/password), and any custom resource-intensive endpoints. Use req.ip for IP-based throttling and req.env['warden'].user.id (if using Devise) or req.session[:user_id] for user-based throttling.
Use automated tools or scripts to send rapid requests to throttled endpoints (e.g., /users/sign_in, /search). Verify that 429 Too Many Requests responses are received after exceeding the limit. Check the Retry-After header. Test different conditions defined in the initializer (IP vs. email parameter).