Back to Blog
critical SEVERITY6 min read

How Missing Rate Limiting Happens in Express.js Authentication Endpoints and How to Fix It

A critical security vulnerability was discovered in the Apple Store API implementation where three authentication endpoints (`/auth/login`, `/auth/refresh`, `/auth/reset`) lacked rate limiting protection. This allowed unlimited authentication attempts from a single IP address, enabling credential stuffing and brute force attacks. The fix implements an in-memory rate limiter that restricts each IP to 5 requests per 15-minute window.

O
By Orbis AppSec
Published August 19, 2026Reviewed August 19, 2026

Answer Summary

Missing rate limiting (CWE-307) in Express.js authentication endpoints allows attackers to perform unlimited login attempts, enabling brute force and credential stuffing attacks. The fix involves implementing IP-based rate limiting middleware that tracks request counts per IP address and rejects requests exceeding a threshold (e.g., 5 attempts per 15 minutes). This vulnerability was found in the Apple Store API's `/auth/login`, `/auth/refresh`, and `/auth/reset` endpoints and fixed by adding a `_checkAuthRate` function that uses a Map to track request windows.

Vulnerability at a Glance

cweCWE-307 (Improper Restriction of Excessive Authentication Attempts)
fixAdded IP-based rate limiter allowing max 5 requests per 15-minute window
riskCredential stuffing, brute force attacks, account takeover
languageJavaScript (Express.js)
root causeAuthentication endpoints lacked request throttling middleware
vulnerabilityMissing Rate Limiting on Authentication Endpoints

Introduction

In the Apple Store API implementation (JS/ipaTool/JS/JS-1/AppleStoreAPI.js), three critical authentication endpoints were left completely unprotected against automated attacks. The /auth/login, /auth/refresh, and /auth/reset routes at line 990 and beyond accepted unlimited requests from any source—no throttling, no IP tracking, no progressive delays.

This meant an attacker could fire thousands of login attempts per second, testing stolen credential lists or brute-forcing passwords without any resistance from the server. The AuthService class handled authentication logic, but the Express route handlers simply processed every incoming request without question.

For developers building authentication systems, this vulnerability demonstrates why security controls must be implemented at the route level, not just within business logic.

The Vulnerability Explained

What Was Missing

Looking at the original code structure, the authentication endpoints were defined as straightforward Express.js POST handlers:

// 登录接口
app.post("/auth/login", async (req, res, next) => {
  const { appleId, password, code } = req.body;
  validate(appleId && password, "缺少必要参数: appleId 和 password");
  // ... authentication logic
});

// 刷新Cookie接口
app.post("/auth/refresh", async (req, res, next) => {
  await AuthService.refreshCookie();
  // ... response handling
});

// 重置登录状态和缓存接口
app.post("/auth/reset", async (req, res, next) => {
  // ... reset logic
});

Notice what's absent: no middleware checking request frequency, no IP tracking, no blocking mechanism. Every request, regardless of origin or history, received full processing.

The Attack Scenario

An attacker targeting this Apple Store API could exploit the missing rate limiting in several ways:

  1. Credential Stuffing: Using leaked username/password combinations from data breaches, an attacker scripts automated attempts against /auth/login. With no rate limiting, they could test millions of credential pairs against legitimate Apple IDs.

  2. Brute Force Attack: For a known appleId, an attacker could systematically try password variations. At 1,000 requests per second, a 6-character lowercase password could be cracked in under 9 hours.

  3. Session Exhaustion: Repeated calls to /auth/refresh could exhaust server resources or trigger Apple's own rate limits, causing denial of service for legitimate users.

The AuthService.login() method would faithfully attempt authentication for each request, potentially triggering account lockouts on Apple's side while the attacker remained undetected by the application itself.

The Fix

The fix introduces an elegant in-memory rate limiting mechanism directly before each authentication endpoint. Here's the implementation added at line 989:

Before (Vulnerable)

app.post("/auth/login", async (req, res, next) => {
  const { appleId, password, code } = req.body;
  validate(appleId && password, "缺少必要参数: appleId 和 password");
  // Processes every request without restriction

After (Fixed)

// 认证接口速率限制 (最多5次/15分钟/IP)
const _authRateLimits = new Map();
const _checkAuthRate = (req, res) => {
  const ip = (req.headers && req.headers["x-forwarded-for"]) || req.ip || "0";
  const now = Date.now(), win = 900000, max = 5;
  let r = _authRateLimits.get(ip) || { c: 0, t: now + win };
  if (now > r.t) { r.c = 0; r.t = now + win; }
  if (++r.c > max) { 
    _authRateLimits.set(ip, r); 
    res.json(createResponse(false, null, "请求过于频繁,请稍后再试")); 
    return false; 
  }
  _authRateLimits.set(ip, r);
  return true;
};

// 登录接口
app.post("/auth/login", async (req, res, next) => {
  if (!_checkAuthRate(req, res)) return;  // Rate limit check added
  const { appleId, password, code } = req.body;
  validate(appleId && password, "缺少必要参数: appleId 和 password");

How It Works

The _checkAuthRate function implements a sliding window rate limiter:

  1. IP Extraction: Retrieves the client IP from x-forwarded-for header (for proxied requests) or falls back to req.ip

  2. Window Management: Uses a 15-minute window (win = 900000 milliseconds) with a maximum of 5 requests (max = 5)

  3. Counter Logic:
    - If the current time exceeds the window expiry (now > r.t), reset the counter
    - Increment the counter for each request
    - If counter exceeds maximum, reject with a friendly error message

  4. State Storage: Uses a Map for O(1) lookup and storage of rate limit state per IP

The fix was applied to all three vulnerable endpoints:
- /auth/login (line 993)
- /auth/refresh (line 1017)
- /auth/reset (line 1027)

Additionally, the code includes a minor refactor on lines 341-342, changing from Object.assign() to spread syntax for the cached login response—a cleaner pattern that doesn't mutate the original object.

Prevention & Best Practices

Implement Rate Limiting Early

Don't wait for a security audit to add rate limiting. Include it as part of your initial endpoint design:

const rateLimit = require('express-rate-limit');

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: { success: false, error: 'Too many attempts, please try again later' },
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/auth/', authLimiter);

Layer Your Defenses

Rate limiting is one layer. Consider also:

  • Account lockout: Temporarily lock accounts after N failed attempts
  • Progressive delays: Increase response time after each failed attempt
  • CAPTCHA: Add challenges after suspicious patterns
  • Anomaly detection: Monitor for unusual login patterns (time, location, device)

Use Distributed Storage for Scale

The in-memory Map approach works for single-server deployments, but use Redis or similar for distributed systems:

const RedisStore = require('rate-limit-redis');

const limiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:auth:',
  }),
  // ... other options
});

Monitor and Alert

Log rate limit triggers and set up alerts for unusual patterns:

if (++r.c > max) {
  console.warn(`Rate limit exceeded for IP: ${ip}`);
  // Send to monitoring system
  metrics.increment('auth.rate_limit.exceeded', { ip });
}

Key Takeaways

  • Authentication endpoints are high-value targets: The /auth/login, /auth/refresh, and /auth/reset routes in AppleStoreAPI.js were prime targets for automated attacks due to their sensitive nature
  • In-memory rate limiting is a valid quick fix: The _checkAuthRate function using a Map provides immediate protection without external dependencies
  • Always check x-forwarded-for behind proxies: The fix correctly extracts the real client IP from proxy headers, preventing attackers from bypassing limits via direct connections
  • 15-minute windows with 5-attempt limits are reasonable defaults: These values balance security (blocking brute force) with usability (allowing legitimate retry attempts)
  • Apply rate limiting consistently: All three auth endpoints received the same protection—inconsistent application leaves attack vectors open

How Orbis AppSec Detected This

  • Source: HTTP POST requests to /auth/login, /auth/refresh, and /auth/reset endpoints in AppleStoreAPI.js
  • Sink: Direct invocation of AuthService.login(), AuthService.refreshCookie(), and cache reset operations without request throttling
  • Missing control: No rate limiting middleware, IP-based throttling, or progressive delay mechanism on authentication routes
  • CWE: CWE-307 (Improper Restriction of Excessive Authentication Attempts)
  • Fix: Added _checkAuthRate function implementing IP-based rate limiting (5 requests per 15-minute window) applied to all three authentication endpoints

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

Missing rate limiting on authentication endpoints is a deceptively simple vulnerability with severe consequences. The Apple Store API's unprotected /auth/login, /auth/refresh, and /auth/reset routes could have enabled credential stuffing attacks at scale, potentially compromising user accounts and Apple Store access.

The fix demonstrates that effective rate limiting doesn't require complex infrastructure—a well-designed in-memory solution with proper IP tracking and window management provides immediate protection. However, for production systems at scale, consider using battle-tested libraries like express-rate-limit with distributed storage backends.

Remember: every authentication endpoint you create is a target. Build rate limiting into your security architecture from day one, not as an afterthought.

References

Frequently Asked Questions

What is missing rate limiting vulnerability?

Missing rate limiting occurs when an application fails to restrict the number of requests a user or IP can make within a time period, allowing attackers to perform brute force attacks, credential stuffing, or denial of service without being blocked.

How do you prevent missing rate limiting in Express.js?

Implement rate limiting middleware using libraries like `express-rate-limit`, or create custom middleware that tracks request counts per IP address using in-memory storage (Map/Object), Redis, or a database, and reject requests exceeding defined thresholds.

What CWE is missing rate limiting?

CWE-307 (Improper Restriction of Excessive Authentication Attempts) covers vulnerabilities where applications don't limit authentication attempts, and CWE-799 (Improper Control of Interaction Frequency) addresses broader rate limiting issues.

Is CAPTCHA enough to prevent brute force attacks?

CAPTCHA alone is not sufficient. Modern CAPTCHA-solving services can bypass these controls. Effective protection requires layered defenses including rate limiting, account lockout policies, multi-factor authentication, and monitoring for suspicious patterns.

Can static analysis detect missing rate limiting?

Yes, static analysis tools can detect authentication endpoints lacking rate limiting middleware by analyzing route definitions and checking for the presence of throttling mechanisms. Tools like Semgrep can be configured with custom rules to flag unprotected auth routes.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

critical

How Distributed Lock Takeover Happens in Node.js and How to Fix It

A critical vulnerability in `redis-lock/server.mjs` allowed any authenticated client to release another client's lock by guessing predictable holder identifiers like process IDs or hostnames. The fix implements cryptographically random `lockId` values that are minted on lock acquisition and validated on release, eliminating the exploit primitive entirely.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.

high

How Information Disclosure via Unstripped Credential Headers Happens in Electron Apps and How to Fix It

A high-severity vulnerability (CVE-2026-54673) in the builder-util-runtime package allowed sensitive credential headers to leak during HTTP redirects in Electron applications. The fix upgrades builder-util-runtime from version 9.5.1 to 9.7.0, which properly strips authentication headers before following redirects to prevent information disclosure.

high

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

high

How Missing CSRF Middleware happens in Express.js and how to fix it

A high-severity CSRF vulnerability was discovered in `libProxy.js` of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a `csrf` token library, a `/csrf-token` endpoint to issue tokens, and a middleware that validates `x-csrf-token` headers or `_csrf` body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with ot