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.

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

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 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.