Overview
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.
- Brute-forcing tokens: Guessing password reset tokens, 2FA codes, or session IDs.
- Denial of Service (DoS): Overwhelming resource-intensive endpoints (like search or complex calculations) with excessive requests.
- API Abuse: Exceeding fair usage quotas for API endpoints.
- Web Scraping: Extracting large amounts of data rapidly. ⏱️💥
Business Impact
Missing rate limiting can lead to:- Account Takeover: Successful brute-force attacks compromise user accounts.
- Denial of Service: Legitimate users may be locked out or experience slow performance due to resource exhaustion caused by automated attacks.
- Increased Costs: Excessive use of resource-intensive functions or paid API calls can inflate operational costs.
- Data Scraping: Competitors or attackers can easily scrape large volumes of public or semi-public data.
Reference Details
CWE ID: CWE-799
OWASP Top 10 (2021): A04:2021 - Insecure Design
Severity: Medium to High
Framework-Specific Analysis and Remediation
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.
- Python
- Java
- .NET(C#)
- PHP
- Node.js
- Ruby
Framework Context
Using libraries likedjango-ratelimit, Flask-Limiter, or implementing custom logic using caching backends (Redis, Memcached).Vulnerable Scenario 1: No Login Attempt Limit (Django)
Copy
# accounts/views.py
# Standard Django LoginView without rate limiting applied
class 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)
Copy
# 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)
Mitigation and Best Practices
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.Secure Code Example
Copy
# views.py (Django with django-ratelimit)
from ratelimit.decorators import ratelimit
class 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 Limiter
from flask_limiter.util import get_remote_address
limiter = 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 endpoint
def api_search_secure():
query = request.args.get('q')
results = perform_complex_search(query)
return jsonify(results)
Testing Strategy
Use automated tools (likewfuzz, 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).Framework Context
Using libraries likeBucket4j, Resilience4j, Spring Cloud Gateway’s RequestRateLimiter filter, or custom implementations with caching (e.g., Guava Cache, Caffeine, Redis).Vulnerable Scenario 1: No Login Throttling
A standard Spring Security form login configuration without any mechanism to limit failed attempts.Copy
// config/SecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin() // DANGEROUS: Default formLogin has no built-in rate limiting.
.permitAll()
.and()
.logout()
.permitAll();
}
Vulnerable Scenario 2: Public API without Limits
A REST controller exposes an endpoint without any rate limiting applied.Copy
// controller/DataController.java
@RestController
public 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);
}
}
Mitigation and Best Practices
Integrate a rate-limiting library likeBucket4j via filters or aspects. Apply limits based on IP address for anonymous endpoints and user ID (from Authentication principal) for authenticated endpoints.Secure Code Example
Copy
// pom.xml (Add Bucket4j dependency)
// <dependency>
// <groupId>com.bucket4j</groupId>
// <artifactId>bucket4j-core</artifactId>
// <version>...</version>
// </dependency>
// Optional: bucket4j-jcache, bucket4j-redis etc. for distributed cache
// config/RateLimitingFilter.java (Example using Bucket4j + Cache)
import io.github.bucket4j.*;
import io.github.bucket4j.grid.GridBucketState;
import io.github.bucket4j.grid.ProxyManager;
import io.github.bucket4j.grid.jcache.JCache; // Example using JCache (e.g., EhCache, Hazelcast)
import javax.cache.Cache; // JCache API
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
// Simplified Filter - Production requires proper cache setup and configuration
public class RateLimitingFilter implements Filter {
private ProxyManager<String> buckets; // Key = IP address
private Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1)); // 10 requests/min
public RateLimitingFilter(Cache<String, GridBucketState> cache) {
// SECURE: Use a distributed cache (JCache, Redis, Hazelcast) for multi-instance apps
this.buckets = Bucket4j.extension(JCache.class).proxyManagerForCache(cache);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Apply only to specific paths, e.g., login
if (!httpRequest.getRequestURI().equals("/login")) { // Example path check
chain.doFilter(request, response);
return;
}
String ipAddress = httpRequest.getRemoteAddr(); // Basic IP check
Bucket bucket = buckets.getProxy(ipAddress, () -> BucketConfiguration.builder().addLimit(limit).build());
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
httpResponse.setHeader("X-Rate-Limit-Remaining", String.valueOf(probe.getRemainingTokens()));
chain.doFilter(request, response);
} else {
long waitForRefill = Duration.ofNanos(probe.getNanosToWaitForRefill()).toMillis();
httpResponse.setHeader("X-Rate-Limit-Retry-After-Milliseconds", String.valueOf(waitForRefill));
httpResponse.setStatus(429); // Too Many Requests
httpResponse.getWriter().write("Rate limit exceeded.");
}
}
}
Testing Strategy
Use automated tools or scripts to send rapid requests to login endpoints and APIs. Verify that a429 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.Framework Context
Using middleware likeAspNetCoreRateLimit or building custom rate limiting logic, often leveraging IDistributedCache (e.g., Redis).Vulnerable Scenario 1: No Rate Limiting Middleware Configured
A standard ASP.NET Core application without any rate limiting configured inStartup.cs.Copy
// Startup.cs (ConfigureServices and Configure)
// DANGEROUS: No rate limiting services registered or middleware applied.
// Login endpoints, API endpoints are unprotected.
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); /* ... */ }
public void Configure(IApplicationBuilder app) { /* ... app.UseRouting(); app.UseEndpoints(); ... */ }
Vulnerable Scenario 2: Expensive API Call Unprotected
An API endpoint performs a complex calculation but lacks specific rate limits.Copy
// 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);
}
}
Mitigation and Best Practices
Install and configureAspNetCoreRateLimit. 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.Secure Code Example
Copy
// Startup.cs (Secure - ConfigureServices)
using AspNetCoreRateLimit; // Import namespace
public void ConfigureServices(IServiceCollection services)
{
// Needed for configuration loading
services.AddOptions();
// Needed for storing counters (use AddDistributedMemoryCache for single server, AddStackExchangeRedisCache etc. for distributed)
services.AddMemoryCache(); // Or services.AddStackExchangeRedisCache(...)
// Load general configuration from appsettings.json
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
// Load endpoint specific configuration
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
// Inject counter and rules stores
services.AddInMemoryRateLimiting(); // Or AddDistributedRateLimiting<distributed_cache_counters_store>()
// The resolving contributor needs to be Singleton
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
services.AddControllersWithViews();
// ... other services ...
}
// Startup.cs (Secure - Configure)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... other middleware ...
// SECURE: Apply the IP rate limiting middleware
app.UseIpRateLimiting();
app.UseRouting();
app.UseAuthentication(); // Important: Place rate limiting potentially before Auth for anon endpoints
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); /* ... */ });
}
// appsettings.json (Secure - Example Configuration)
{
"IpRateLimiting": {
"EnableEndpointRateLimiting": true, // Apply endpoint rules
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP", // If behind proxy
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"GeneralRules": [
{ "Endpoint": "*:/api/login", "Period": "1m", "Limit": 5 }, // Limit login attempts
{ "Endpoint": "*", "Period": "10s", "Limit": 20 } // General limit
]
},
"IpRateLimitPolicies": {
// Add specific policies if needed
}
// ... other settings ...
}
Testing Strategy
Use automated tools or scripts to flood endpoints defined in theAspNetCoreRateLimit 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.Framework Context
Using Laravel’s built-inthrottle middleware or implementing custom logic with caching (Redis, Memcached).Vulnerable Scenario 1: Login Route Without Throttling (Older Laravel)
In older Laravel versions, throttling wasn’t applied to login routes by default.Copy
// routes/web.php (Older Laravel)
// DANGEROUS: The default login route might lack rate limiting.
Auth::routes();
Vulnerable Scenario 2: API Route Missing Throttle Middleware
A developer creates an API route but forgets to apply thethrottle middleware.Copy
// routes/api.php
use App\Http\Controllers\DataController;
// DANGEROUS: This route has no rate limit.
Route::get('/data/{id}', [DataController::class, 'show']);
// This route group is protected
Route::middleware('auth:sanctum', 'throttle:60,1')->group(function () {
Route::get('/user', function (Request $request) { return $request->user(); });
});
Mitigation and Best Practices
Apply thethrottle 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.Secure Code Example
Copy
// 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 IP
Route::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(); });
});
Copy
// 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',
];
Testing Strategy
Use automated tools or scripts to send rapid requests to login routes and API endpoints. Verify that429 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.Framework Context
Using middleware libraries likeexpress-rate-limit or rate-limiter-flexible.Vulnerable Scenario 1: No Rate Limiting Middleware Applied
An Express application without any rate limiting middleware.Copy
// app.js
const express = require('express');
const app = express();
app.post('/login', (req, res) => {
// DANGEROUS: No limit on login attempts.
// ... authentication logic ...
});
app.get('/api/data', (req, res) => {
// DANGEROUS: No limit on API calls.
res.json({ data: 'some data' });
});
app.listen(3000);
Vulnerable Scenario 2: Rate Limiter Applied After Routes
The rate limiting middleware is added too late in the middleware chain.Copy
// app.js
const 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' }); });
Mitigation and Best Practices
Applyexpress-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.Secure Code Example
Copy
// app.js (Secure)
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// SECURE: Apply a general limiter early. Affects all subsequent routes.
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes'
});
app.use(generalLimiter);
// SECURE: Apply stricter limiter specifically for login attempts.
const loginLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
max: 5, // Limit each IP to 5 login requests per windowMs
message: 'Too many login attempts from this IP, please try again after 10 minutes',
keyGenerator: (req, res) => {
// Track by IP + attempted username (if available) for better granularity
return req.ip + (req.body.username || '');
}
});
app.post('/login', loginLimiter, (req, res) => {
// ... authentication logic ...
});
// This API endpoint is covered by the generalLimiter applied earlier
app.get('/api/data', (req, res) => {
res.json({ data: 'some data' });
});
app.listen(3000);
Testing Strategy
Use automated tools or scripts to send rapid requests to endpoints protected byexpress-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.Framework Context
Using gems likerack-attack as middleware.Vulnerable Scenario 1: No rack-attack Configured
A standard Rails application without the rack-attack gem installed or configured in config/initializers/rack_attack.rb.Copy
# 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.Copy
# config/initializers/rack_attack.rb
Rack::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.rb
def index
# Assumes perform_expensive_search is resource intensive
@results = perform_expensive_search(params[:query])
# ... render ...
end
Mitigation and Best Practices
Install and configurerack-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.Secure Code Example
Copy
# Gemfile
gem 'rack-attack'
# config/application.rb
module MyApp
class Application < Rails::Application
# ...
# SECURE: Apply Rack::Attack middleware
config.middleware.use Rack::Attack
# ...
end
end
# config/initializers/rack_attack.rb
class Rack::Attack
# Throttle requests to sensitive paths by IP address
# Limit: 5 requests per minute (adjust as needed)
throttle('req/ip', limit: 300, period: 5.minutes, &:ip)
# Throttle login attempts by IP address
# Limit: 5 attempts per minute per IP
throttle('logins/ip', limit: 5, period: 60.seconds) do |req|
if req.path == '/users/sign_in' && req.post?
req.ip
end
end
# Throttle login attempts by email parameter (more specific)
throttle('logins/email', limit: 5, period: 60.seconds) do |req|
if req.path == '/users/sign_in' && req.post?
# Ensure email param exists and is parseable; handle nil case
req.params['user']['email'].to_s.downcase.strip.presence
end
end
# SECURE: Throttle expensive search endpoint
throttle('search/ip', limit: 10, period: 60.seconds) do |req|
req.ip if req.path == '/search'
end
# Optional: Block suspicious requests (e.g., scanners)
blocklist('fail2ban pentesters') do |req|
# `filter` returns truthy value if request fails, or if it's potentially malicious
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 1.hour) do
# Match paths used by common scanning tools
req.path.include?('/etc/passwd') || req.path.include?('wp-admin')
end
end
# Customize response for throttled requests
self.throttled_response = lambda do |env|
retry_after = (env['rack.attack.match_data'] || {})[:period]
headers = {
'Content-Type' => 'application/json',
'Retry-After' => retry_after.to_s
}
[ 429, headers, [{ error: "Throttle limit reached. Retry later." }.to_json] ]
end
end
Testing Strategy
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).
