Back to Blog
high SEVERITY6 min read

How Missing Rate Limiting Leads to Denial of Service in Express.js and How to Fix It

A high-severity denial of service vulnerability in `src/index.js` allowed attackers to exhaust server resources through unlimited requests to `/api/register` and `/api/captcha`. The fix implements in-memory rate limiting middleware, restricting captcha generation to 20 requests per minute and registration to 10 requests per minute per IP.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

This is a **Missing Rate Limiting** vulnerability (CWE-770) in an Express.js application at `src/index.js:1511`. The `/api/register` and `/api/captcha` endpoints lacked any throttling, enabling resource exhaustion attacks through bcrypt CPU consumption, database writes, and captcha generation. The fix adds a custom `apiRateLimit()` middleware that tracks per-IP request counts with 60-second windows, returning HTTP 429 when limits are exceeded.

Vulnerability at a Glance

cweCWE-770 (Allocation of Resources Without Limits or Throttling)
fixCustom in-memory rate limiter with per-IP tracking and 429 responses
riskDenial of Service via resource exhaustion (CPU, memory, database)
languageJavaScript (Node.js/Express)
root causeExpress routes registered without rate limiting middleware
vulnerabilityMissing Rate Limiting

Introduction

In a production Express.js application, the src/index.js file handles critical user-facing operations including account registration and CAPTCHA generation. At lines 1450 and 1531, two endpoints—/api/captcha and /api/register—were exposed without any request throttling. This oversight created a high-severity denial of service vulnerability: attackers could flood these endpoints with unlimited requests, exhausting CPU through bcrypt password hashing, overwhelming the database with write operations, and draining memory with CAPTCHA generation.

The vulnerability is particularly dangerous because both endpoints trigger expensive operations. The registration endpoint hashes passwords with bcrypt—a deliberately slow algorithm—while the CAPTCHA endpoint generates SVG graphics and maintains in-memory state. Without rate limiting, a single attacker could degrade service for all legitimate users.

The Vulnerability Explained

The Vulnerable Code

Before the fix, the Express routes were registered with no protective middleware:

// src/index.js:1450 (BEFORE)
app.get('/api/captcha', (req, res) => {
  cleanupCaptchas();
  const payload = generateCaptcha();
  res.json({ ok: true, token: payload.token, svg: payload.svg });
});

// src/index.js:1531 (BEFORE)
app.post('/api/register', async (req, res) => {
  try {
    const { username, password, email, captchaToken, captchaCode, inviteCode } = req.body || {};
    // ... bcrypt password hashing, database writes
  }
});

Why This Is Dangerous

The specific threat model for this application includes:

  1. CPU Exhaustion via bcrypt: Each registration triggers bcrypt.hash() on the password. At cost factor 10+, each hash takes ~100ms. An attacker sending 100 requests/second consumes 10 CPU cores.

  2. Memory Pressure from CAPTCHAs: The generateCaptcha() function creates SVG data and stores tokens in memory. Unlimited generation fills RAM and triggers garbage collection storms.

  3. Database Write Amplification: Each registration attempt writes to the users table, potentially triggering index rebuilds and lock contention.

  4. CAPTCHA Bypass Economics: Even with CAPTCHA validation, attackers can automate solving (via services like 2captcha) at scale if volume isn't restricted.

Real-World Attack Scenario

An attacker with basic scripting tools could execute:

# Exhaust CPU with parallel registration attempts
while true; do
  curl -X POST http://target/api/register \
    -H "Content-Type: application/json" \
    -d '{"username":"a'$(date +%s)'","password":"x","email":"a@b.c","captchaToken":"stolen","captchaCode":"1234"}' &
done

Within seconds, legitimate users would experience timeouts as the event loop blocks on bcrypt operations.

The Fix

The remediation implements a custom in-memory rate limiter tailored to this application's needs, avoiding external dependencies while providing immediate protection.

The Rate Limiting Middleware

// src/index.js:1450-1468 (NEW)
function apiRateLimit(maxRequests, windowMs) {
  const hits = new Map();
  return (req, res, next) => {
    const key = req.ip || req.connection?.remoteAddress || 'unknown';
    const now = Date.now();
    const entry = hits.get(key);
    if (!entry || now > entry.resetAt) {
      hits.set(key, { count: 1, resetAt: now + windowMs });
      return next();
    }
    if (entry.count >= maxRequests) {
      return res.status(429).json({ error: '请求过于频繁,请稍后再试。' });
    }
    entry.count += 1;
    next();
  };
}

Configured Limits

// src/index.js:1470-1471
const captchaRateLimit = apiRateLimit(20, 60_000);   // 20/minute
const registerRateLimit = apiRateLimit(10, 60_000);  // 10/minute

Protected Routes

// src/index.js:1473 (AFTER)
app.get('/api/captcha', captchaRateLimit, (req, res) => { ... });

// src/index.js:1532 (AFTER)
app.post('/api/register', registerRateLimit, async (req, res) => { ... });

Why This Fix Works

Aspect Implementation Detail Security Benefit
Keying Per-IP via req.ip or remoteAddress Prevents single attacker from exhausting global pool
Windowing 60-second sliding windows Balances burst tolerance with sustained attack protection
Memory safety Map with automatic expiration Old entries naturally purge; prevents unbounded growth
Differentiated limits 20 for CAPTCHA, 10 for register Aligns cost of operation with restriction strictness
User feedback HTTP 429 with Chinese error message Clear signal to legitimate users, standard rejection for attackers

The fix preserves all legitimate functionality—users can still register and request CAPTCHAs—while bounding resource consumption.

Prevention & Best Practices

For Express.js Applications

  1. Adopt express-rate-limit for production: While the custom implementation suffices here, the battle-tested express-rate-limit package provides Redis-backed storage for distributed deployments, IP whitelisting, and standardized headers.

javascript const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, }); app.use('/api/', limiter);

  1. Apply defense in depth: Rate limiting should complement, not replace, input validation, CAPTCHA, and account lockout policies.

  2. Monitor and alert: Log 429 responses to detect attack patterns and tune limits.

  3. Consider cost-based limiting: Expensive operations (bcrypt, PDF generation, external API calls) deserve stricter limits than cheap reads.

Detection Tools

Tool Rule Coverage
Semgrep express-missing-rate-limit Flags Express routes without rate limiting
CodeQL js/missing-rate-limiting Detects missing throttling on expensive operations
ESLint security/detect-rate-limit Custom rule for route analysis

Standards Alignment

Key Takeaways

  • Never expose expensive endpoints without throttling: The /api/register endpoint's bcrypt hashing made it a CPU exhaustion vector; CAPTCHA generation created memory pressure. Both required limits.

  • Differentiate limits by operation cost: The fix applies 10 req/min to registration (expensive) versus 20 req/min to CAPTCHA (cheaper), reflecting actual resource consumption.

  • In-memory rate limiting has trade-offs: The Map-based solution works for single-instance deployments but requires Redis or similar for horizontal scaling—document this architectural constraint.

  • IP-based keying is a baseline, not a ceiling: Sophisticated attackers use botnets; consider adding device fingerprinting or proof-of-work for high-risk endpoints.

  • Error messages should be actionable: The Chinese message "请求过于频繁,请稍后再试" (Request too frequent, please try again later) clearly communicates the temporary nature of the block to legitimate users.

How Orbis AppSec Detected This

Field Details
Source HTTP request to /api/register and /api/captcha endpoints
Sink Unprotected Express route handlers in src/index.js:1450 and src/index.js:1531
Missing control No rate limiting middleware in route handler chain; absence of request throttling before expensive operations (bcrypt.hash(), generateCaptcha())
CWE CWE-770 (Allocation of Resources Without Limits or Throttling)
Fix Added apiRateLimit() middleware with per-IP tracking, 60-second windows, and differentiated limits (10 req/min for register, 20 req/min for captcha)

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The missing rate limiting in src/index.js exemplifies how a single oversight in middleware configuration can expose production systems to trivial denial of service attacks. The fix demonstrates that effective protection doesn't require complex infrastructure—a 23-line custom middleware function, properly integrated at route registration, eliminates the attack surface.

For developers maintaining Express applications, this case underscores the importance of auditing every endpoint for resource consumption characteristics and applying appropriate throttling. The most expensive operations in your application—cryptographic hashing, file generation, external API calls—should be your most protected.

References

Frequently Asked Questions

What is Missing Rate Limiting?

Missing Rate Limiting occurs when an application fails to restrict the number of requests a client can make within a time window, allowing attackers to overwhelm server resources.

How do you prevent Missing Rate Limiting in Express.js?

Implement rate limiting middleware using libraries like `express-rate-limit` or custom implementations that track requests per IP with time windows, rejecting excess requests with HTTP 429.

What CWE is Missing Rate Limiting?

CWE-770 (Allocation of Resources Without Limits or Throttling) and CWE-400 (Uncontrolled Resource Consumption).

Is CAPTCHA enough to prevent automated abuse of registration endpoints?

No. CAPTCHA prevents bot automation but doesn't limit request volume—attackers can still exhaust server resources solving CAPTCHAs or triggering expensive operations like bcrypt password hashing.

Can static analysis detect Missing Rate Limiting?

Yes. Static analyzers can flag Express routes that lack rate limiting middleware by detecting the absence of protective middleware in route handler chains.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #47

Related Articles

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) in the `tar` npm package allowed attackers to craft malicious gzip archives that could exhaust memory or CPU during decompression. The fix upgrades `tar` from 7.5.11 to 7.5.21 across `package.json` and `package-lock.json`, closing the resource-exhaustion path without changing any application code.