Back to Cheat Sheets

⏱️ Lack of Resources & Rate LimitingAPI 2019

OWASP API Security Top 10 2019 - API4

HIGH RISK

📋 What Is It?

Lack of Resources & Rate Limiting is the absence of limits on how often an API can be called and how much work a single call may cost. Every request consumes something finite - CPU, memory, disk, database connections, third-party quota, money - and an API that never caps request rate, page size, payload size, upload size, or query depth lets one client consume everything.

The result is two distinct harms: availability loss (the service degrades or falls over for everyone) and attack amplification (unlimited attempts make brute force, credential stuffing, enumeration, and scraping practical at scale).

API4 OWASP Rank 2019
DoS Primary Impact
Enabler For Brute Force

⚠️ Common Exploits

  • Unbounded Pagination: ?limit=1000000 pulls the whole table into memory
  • Request Flooding: no per-client throttle on expensive endpoints
  • Huge Payloads & Uploads: no body-size cap; zip/image bombs
  • Expensive Queries: deep GraphQL nesting, unbounded search, N+1 explosions
  • ReDoS: catastrophic backtracking in a validation regex
  • Third-Party Cost Burn: unlimited SMS/e-mail/AI calls billed per request
  • Missing Timeouts: slow-loris style connections held open indefinitely

🔴 Attack Flow

1. Measure response cost per endpoint (time, payload size)
↓
2. Find the parameter that scales that cost: limit, depth, size, page
↓
3. Confirm no 429 arrives however fast requests are sent
↓
4. Multiply - concurrency × the most expensive call
↓
5. IMPACT: Service degradation, runaway cloud bill, or unlimited brute force!

❌ Vulnerable Code

# Bad: client-controlled page size, no ceiling @app.route('/api/products') def products(): limit = int(request.args.get('limit', 50)) ← ?limit=1000000 return jsonify([p.to_dict() for p in Product.query.limit(limit)]) // Bad: no body limit, no timeout, no throttle app.use(express.json()); ← default is generous; uploads unbounded app.post('/api/report', async (req, res) => { res.json(await buildHeavyReport(req.body)); // 30s of CPU per call, callable in a loop }); # Bad: catastrophic backtracking on attacker-controlled input if re.match(r'^(a+)+$', user_input): ← ReDoS: one request pins a core ... # Bad: unlimited third-party spend sms.send(request.json['phone'], code) ← each call costs real money

✅ Secure Code

# Good: clamp the page size server-side MAX_LIMIT = 100 limit = min(int(request.args.get('limit', 50)), MAX_LIMIT) // Good: body caps, timeouts, and tiered rate limits app.use(express.json({ limit: '100kb' })); app.use(rateLimit({ windowMs: 60_000, max: 100, keyGenerator: (r) => r.user?.sub ?? r.ip })); app.use('/api/report', rateLimit({ windowMs: 60_000, max: 5 })); // expensive route, tighter cap server.requestTimeout = 15_000; server.headersTimeout = 10_000; // Good: bound query cost, not just request count const schema = applyMiddleware(base, depthLimit(7), costAnalysis({ maximumCost: 1000 })); # Good: safe validation + per-account spend budget if not EMAIL_RE.fullmatch(value, timeout=0.05): # linear-time pattern, bounded raise Invalid() enforce_quota(user, 'sms', per_hour=5, per_day=20) # cap third-party cost # Good: tell the client what the limits are response.headers['RateLimit-Limit'] = '100' response.headers['Retry-After'] = '60' # return 429, not a silent drop

✓ Prevention Checklist

  • Rate-limit per authenticated identity and per source, not per IP alone
  • Set tighter limits on expensive and security-sensitive endpoints
  • Clamp limit/page/size parameters to a server-side maximum
  • Cap request body and upload sizes at the gateway and in the app
  • Bound query cost: GraphQL depth/complexity limits, pagination everywhere
  • Set connection, read, and execution timeouts on every hop
  • Constrain container CPU/memory so one service cannot starve the host
  • Use linear-time validation patterns; avoid catastrophic regex backtracking
  • Apply spend quotas to paid third-party calls (SMS, e-mail, AI, storage)
  • Return 429 with Retry-After and publish the limits
  • Alert on sustained 429s and on cost/latency anomalies

🔍 Detection & Tools

Enforcement:

API gateways (Kong, APIM) nginx limit_req express-rate-limit Cloudflare / WAF

Testing & Monitoring:

k6 / Locust Artillery Prometheus + Grafana Cloud cost alerts

How to Test:

  • Send bursts and sustained load - does a 429 ever appear?
  • Push limit, depth, and body size far past sane values
  • Upload an oversized or highly compressible file
  • Time the slowest endpoint and estimate cost per concurrent client
  • Check limits apply per account, not just per IP (rotate IPs)

🌍 Real-World

These are documented incident classes - recurring patterns rather than single CVEs.

  • Unlimited OTP attempts: no throttle on a verification endpoint turns a 6-digit code into a few minutes of scripted guessing.
  • Scraping through pagination: an unclamped limit parameter lets a competitor pull an entire catalogue or user directory in a handful of calls.
  • Cost-amplification attacks: unmetered SMS, e-mail, or model-inference endpoints turned into a bill the victim pays.
  • ReDoS outages: a single crafted string against a backtracking regex pins CPU and takes an API offline.
  • Zip/image bombs: small uploads that expand to gigabytes in memory during processing.

📌 Quick Tips

  • DO NOT let the client choose how much work you do
  • DO NOT rate-limit by IP alone
  • DO clamp page size, body size, and query depth
  • DO set timeouts on every external call
  • DO budget spend on paid downstream services

📜 Compliance

Related Standards:

  • OWASP API Security Top 10 2019 - API4
  • Renamed to API4:2023 Unrestricted Resource Consumption
  • CWE-770 Allocation of Resources Without Limits or Throttling
  • CWE-400 Uncontrolled Resource Consumption
  • CWE-1333 Inefficient Regular Expression Complexity