API Rate Limiting Testing: Finding Missing Controls Before Attackers Do
OWASP API Security lists "Unrestricted Resource Consumption" (API4) as a top API risk. In practice, this means missing or insufficient rate limiting — and it enables a category of attacks broader than most developers realize. Credential stuffing, brute force against login endpoints, enumeration of user accounts, mass data scraping, SMS OTP bombing, and targeted denial of service are all enabled by the same underlying weakness: no meaningful limit on how many times an attacker can make a request.
Rate limiting is deceptively difficult to get right. An application might rate-limit the public login endpoint but leave the mobile API endpoint unprotected. It might rate-limit by IP address but not by username, allowing distributed credential stuffing to bypass IP-based controls entirely. This guide covers how to test rate limiting controls systematically.
What Rate Limiting Is Actually Protecting
Rate limiting is not a single control — it protects against different threat models depending on what it's applied to:
| Endpoint Type | Attack Enabled Without Rate Limiting | Rate Limit Key |
|---|---|---|
| Login / authentication | Brute force, credential stuffing | IP + username |
| Password reset | Account lockout DoS, token brute force | IP + email |
| OTP / MFA verification | OTP brute force (6-digit codes = 1M possibilities) | IP + session + account |
| Registration | Account enumeration, bulk account creation, SMS abuse | IP + phone/email |
| Search / list endpoints | Data scraping, enumeration | IP + authenticated user |
| Expensive operations (report gen, export) | Resource exhaustion, application-layer DoS | Authenticated user |
| SMS / email send | SMS bombing, spam using your infrastructure | IP + target number/email |
Testing for Missing Rate Limiting
Basic Brute Force Test
The simplest test: send the same request hundreds of times and observe whether the application responds identically throughout or starts rejecting requests.
# Using ffuf to brute force a login endpoint
ffuf -w /usr/share/wordlists/passwords.txt \
-X POST \
-u https://target.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test@example.com","password":"FUZZ"}' \
-fc 401 \
-rate 10
# If all 401s and no rate limiting response (429/lockout) appears,
# the endpoint is not rate limited.
# Using Burp Suite Intruder:
# 1. Capture login request
# 2. Send to Intruder
# 3. Mark password field as payload position
# 4. Use password list as payload
# 5. Observe responses - look for lockout or 429 after N attempts
# Quick check with curl - send 50 rapid requests
for i in $(seq 1 50); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://target.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test@example.com","password":"wrongpassword'$i'"}')
echo "Request $i: $STATUS"
done
# Expected: 429 after N failed attempts
# Vulnerable: All 401 (no rate limiting)
Distributed Credential Stuffing Simulation
IP-based rate limiting alone doesn't prevent credential stuffing from distributed botnets. Test whether the application rate-limits by username/account as well:
# Test: same password, different usernames (account enumeration + stuffing)
# If rate limited by IP but not by account, this bypasses the control
for user in user1@example.com user2@example.com user3@example.com; do
curl -s -o /dev/null -w "$user: %{http_code}\n" \
-X POST https://target.com/api/auth/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"$user\",\"password\":\"Password123\"}"
done
Bypass Techniques to Test
Rate limiting is only as good as the key it uses to identify the client. Test whether each of these bypasses the control:
IP Address Spoofing via Headers
Many applications use the X-Forwarded-For header to get the "real" client IP when behind a proxy. If the application trusts this header without validation, an attacker can rotate IPs by changing the header value.
# Test if IP-based rate limiting can be bypassed via header injection
curl -X POST https://target.com/api/auth/login \
-H "X-Forwarded-For: 1.2.3.4" \
-H "Content-Type: application/json" \
-d '{"username":"test@example.com","password":"wrong"}'
# Rotate the header value on each request:
for i in $(seq 1 50); do
curl -X POST https://target.com/api/auth/login \
-H "X-Forwarded-For: $i.0.0.1" \
-H "Content-Type: application/json" \
-d "{\"username\":\"test@example.com\",\"password\":\"wrong$i\"}"
done
# Other headers to test:
# X-Real-IP
# X-Client-IP
# CF-Connecting-IP
# True-Client-IP
Parameter Variation
Rate limiting keyed on the exact request body can sometimes be bypassed by adding innocuous variations:
# Add extra whitespace
{"username":"test@example.com", "password":"wrong"}
{"username": "test@example.com","password":"wrong"}
# Add unused parameters
{"username":"test@example.com","password":"wrong","_":1}
{"username":"test@example.com","password":"wrong","_":2}
# Vary casing (if username is treated as case-insensitive)
{"username":"Test@example.com","password":"wrong"}
{"username":"TEST@EXAMPLE.COM","password":"wrong"}
# Add null bytes or Unicode variants
{"username":"test@example.com ","password":"wrong"}
Endpoint Aliasing
The same authentication logic may be exposed through multiple paths. Rate limiting applied to one endpoint may not cover the others:
# Check for alternative login paths
/api/auth/login
/api/v1/auth/login
/api/v2/auth/login
/api/login
/login
/auth
/user/login
/account/login
/mobile/auth/login # Mobile-specific endpoints often less protected
/api/auth/token # OAuth token endpoints
HTTP Method Variation
# Rate limiting may apply to POST but not PUT, PATCH, or OPTIONS
curl -X PUT https://target.com/api/auth/login ...
curl -X PATCH https://target.com/api/auth/login ...
# HTTP/2 vs HTTP/1.1 - some rate limiters key on connection-level metadata
# Use h2c or explicit HTTP/2 clients to test
Cookie and Token Rotation
# Rate limiting by session token - does regenerating the token reset the counter?
# 1. Send N-1 requests (just below limit)
# 2. Clear cookies / get a new session
# 3. Continue from a fresh session
# If the counter resets, rate limiting is keyed to the session, not the account
Testing OTP and MFA Endpoints
OTP brute force is a critical test. A 6-digit numeric OTP has 1,000,000 possibilities. Without rate limiting, an attacker can exhaust this space in hours. Most OTPs expire in 5-10 minutes, narrowing the window — but if the rate limit is generous (say, 100 attempts before lockout), and each OTP attempt takes 100ms, the attacker has a meaningful probability of success in a 10-minute window.
# Test OTP brute force
# Capture an OTP verification request first
# Then use Burp Intruder with a number sequence payload
POST /api/auth/verify-otp
{"otp": "PAYLOAD", "session_token": "abc123"}
# Payload: number sequence 000000 to 999999
# Observe: does the application lock out after N attempts?
# Observe: is there a consistent time delay that indicates throttling?
Also test whether OTP codes are invalidated after a failed attempt (they should be), and whether an OTP can be used more than once (it shouldn't be).
Resource Exhaustion Testing
Beyond authentication, rate limiting protects computationally expensive endpoints. Test:
# Report generation or data export
for i in $(seq 1 20); do
curl -X POST https://target.com/api/reports/generate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"full_export","format":"pdf"}' &
done
wait
# Search with expensive queries
for i in $(seq 1 50); do
curl "https://target.com/api/search?q=a&include=all&depth=10" \
-H "Authorization: Bearer $TOKEN" &
done
wait
# Observe: Does response time increase? Does the server start returning 503?
# Does the application enforce a queue or concurrent request limit?
Checking Rate Limit Response Headers
Well-implemented rate limiting communicates its state via response headers. Check for these:
# Make a request and inspect headers
curl -I -X POST https://target.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test@example.com","password":"wrong"}'
# Look for:
# X-RateLimit-Limit: 10 (max requests in window)
# X-RateLimit-Remaining: 9 (requests remaining)
# X-RateLimit-Reset: 1719000000 (Unix timestamp when limit resets)
# Retry-After: 30 (seconds to wait when 429'd)
# Absence of these headers doesn't mean no rate limiting,
# but presence helps you understand the policy.
Rate Limit Testing Checklist
- Login endpoint — test brute force with wrong passwords, observe lockout behavior
- Password reset — test both IP-based and email-based rate limiting
- OTP/MFA verification — test exhaustion of 6-digit code space
- Registration — test bulk account creation and phone/email verification abuse
- Search and list endpoints — test enumeration via pagination
- Report/export generation — test concurrent request limits
- Test IP spoofing via X-Forwarded-For header rotation
- Test endpoint aliasing — same logic at different paths
- Test parameter variation to bypass key-based rate limiting
- Test authenticated vs unauthenticated limits — often different policies
- Check rate limit response headers (X-RateLimit-*)
- Test mobile API endpoints separately — they often have looser controls
Remediation
Rate-limit by account, not just IP. IP-based rate limiting is bypassed trivially with distributed infrastructure. Rate limiting keyed on the target account (username, email, user ID) prevents credential stuffing even from distributed sources. Combine IP + account limiting for best coverage.
Apply progressive delays before hard lockouts. Account lockout is a DoS tool — an attacker who knows the lockout threshold can continuously lock out any account. Consider progressive delays (increasing wait times after each failed attempt) instead of or in addition to hard lockouts.
Don't trust X-Forwarded-For for rate limiting unless you control the proxy. If your application is behind a load balancer that sets X-Forwarded-For, trust it only from the load balancer's IP. Reject or ignore the header from other sources.
Implement separate limits for authentication-related endpoints. Login, password reset, and OTP verification should have stricter limits than general API endpoints. Five failed OTP attempts before lockout is appropriate. Five hundred is not.
Invalidate OTP codes after each failed attempt. A rate-limited OTP that survives failed attempts gives attackers multiple windows per code. Invalidate the code on first failure and require a new code to be sent.
Test rate limiting as part of your security assessment, not as an afterthought. Rate limiting configuration often diverges between environments (dev has none, staging has loose limits, production has inconsistent coverage). Automated scanning catches the gaps before they become incidents.
Ironimo tests authentication endpoints, password reset flows, and sensitive API operations for missing or insufficient rate limiting — catching the controls that attackers probe first. It runs the same checks a manual pentester would perform, automatically, on your schedule.
Start free scan