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:
-
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. -
Memory Pressure from CAPTCHAs: The
generateCaptcha()function creates SVG data and stores tokens in memory. Unlimited generation fills RAM and triggers garbage collection storms. -
Database Write Amplification: Each registration attempt writes to the users table, potentially triggering index rebuilds and lock contention.
-
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
- Adopt
express-rate-limitfor production: While the custom implementation suffices here, the battle-testedexpress-rate-limitpackage 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);
-
Apply defense in depth: Rate limiting should complement, not replace, input validation, CAPTCHA, and account lockout policies.
-
Monitor and alert: Log 429 responses to detect attack patterns and tune limits.
-
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
- OWASP API Security Top 10 2023: API4:2023 – Unrestricted Resource Consumption
- CWE-770: Allocation of Resources Without Limits or Throttling
- CWE-400: Uncontrolled Resource Consumption
Key Takeaways
-
Never expose expensive endpoints without throttling: The
/api/registerendpoint'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
- CWE-770: Allocation of Resources Without Limits or Throttling
- CWE-400: Uncontrolled Resource Consumption
- OWASP API Security Top 10 2023 – API4: Unrestricted Resource Consumption
- express-rate-limit documentation
- Semgrep rule: express-missing-rate-limit
- fix: no rate limiting middleware is implemented in t... in index.js