Back to Blog
high SEVERITY7 min read

How in-memory rate limiting vulnerabilities happen in Node.js APIs and how to fix it

A high-severity rate limiting vulnerability was discovered in the Everclaw Key API's server.js file, where the checkIpRateLimit() function used in-memory storage that reset on server restarts and didn't synchronize across multiple instances. The fix migrates to Redis-backed rate limiting, ensuring persistent, distributed protection against API key request abuse.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 5, 2026•Reviewed September 5, 2026

Answer Summary

This is an in-memory rate limiting vulnerability (related to CWE-770: Allocation of Resources Without Limits or Throttling) in a Node.js Express API. The checkIpRateLimit() function in everclaw-key-api/server.js stored rate limit counters in a JavaScript Map, which was lost on server restart and didn't synchronize across load-balanced instances. The fix implements Redis-backed rate limiting using INCR and EXPIRE commands, ensuring the 10 requests per 60-second window limit persists across restarts and is shared across all server instances.

Vulnerability at a Glance

cweCWE-770 (Allocation of Resources Without Limits or Throttling)
fixMigrate to Redis-backed rate limiting with atomic INCR operations
riskAttackers can exceed rate limits via server restarts or multi-instance deployments
languageJavaScript (Node.js)
root causeRate limit state stored in local Map() instead of shared persistent storage
vulnerabilityIn-memory rate limiting bypass

Introduction

In the Everclaw Key API repository, we discovered a high-severity rate limiting vulnerability in everclaw-key-api/server.js at line 105. The /api/keys/request endpoint, which handles API key generation and renewal, implemented IP-based rate limiting through the checkIpRateLimit() function—but with a critical architectural flaw. The function stored rate limit counters in an in-memory JavaScript Map(), meaning every server restart wiped the slate clean, and multiple server instances each maintained their own separate counters.

This matters because rate limiting is often the first line of defense against automated abuse. When that defense has holes you can drive a truck through, attackers can exhaust resources, generate unlimited API keys, or bypass intended access controls simply by timing their attacks around deployments or exploiting load balancer distribution.

The Vulnerability Explained

Let's examine the original vulnerable code in server.js:

const ipRequestCounts = new Map();

function checkIpRateLimit(ip) {
  const now = Date.now();
  const entry = ipRequestCounts.get(ip);
  if (!entry || now - entry.windowStart > KEY_REQUEST_WINDOW_MS) {
    ipRequestCounts.set(ip, { windowStart: now, count: 1 });
    return true;
  }
  entry.count++;
  ipRequestCounts.set(ip, entry);
  return entry.count <= KEY_REQUEST_MAX_PER_WINDOW;
}

The function enforces 10 requests per 60-second window (KEY_REQUEST_MAX_PER_WINDOW = 10, KEY_REQUEST_WINDOW_MS = 60 * 1000). The problem? The ipRequestCounts Map lives entirely in Node.js process memory.

Specific Attack Scenarios:

  1. Server Restart Bypass: An attacker hits the endpoint 10 times, exhausting their limit. They wait 30 seconds (not the full 60), then trigger a server restart (via a separate DoS attack, deployment, or crash). When the server comes back up, ipRequestCounts is empty—they can immediately make 10 more requests. Repeat indefinitely.

  2. Multi-Instance Bypass: In a production environment with 3 load-balanced instances behind NGINX, each instance maintains its own ipRequestCounts Map. An attacker can make 10 requests to instance A, 10 to instance B, and 10 to instance C—effectively 30 requests in the same 60-second window, tripling the intended limit.

  3. Sustained Distributed Attack: An attacker with a botnet of 100 IPs can generate 1,000 API keys per minute (10 per IP) instead of the intended 10 total, especially if they cycle through instances or time attacks around deployments.

The real-world impact for the Everclaw Key API is severe: unlimited API key generation could enable downstream abuse of whatever services those keys unlock, resource exhaustion on key validation endpoints, or database bloat from storing excessive keys.

The Fix

The fix migrates rate limiting to Redis, a persistent, shared data store. Here's the specific change to checkIpRateLimit():

Before (lines 50-60):

function checkIpRateLimit(ip) {
  const now = Date.now();
  const entry = ipRequestCounts.get(ip);
  if (!entry || now - entry.windowStart > KEY_REQUEST_WINDOW_MS) {
    ipRequestCounts.set(ip, { windowStart: now, count: 1 });
    return true;
  }
  entry.count++;
  ipRequestCounts.set(ip, entry);
  return entry.count <= KEY_REQUEST_MAX_PER_WINDOW;
}

After (lines 50-68):

async function checkIpRateLimit(ip) {
  // Prefer Redis so the limit survives restarts and is shared across instances.
  if (redis) {
    const key = `ratelimit:keys:${ip}`;
    const count = await redis.incr(key);
    if (count === 1) {
      await redis.expire(key, Math.ceil(KEY_REQUEST_WINDOW_MS / 1000));
    }
    return count <= KEY_REQUEST_MAX_PER_WINDOW;
  }
  const now = Date.now();
  const entry = ipRequestCounts.get(ip);
  if (!entry || now - entry.windowStart > KEY_REQUEST_WINDOW_MS) {
    ipRequestCounts.set(ip, { windowStart: now, count: 1 });
    return true;
  }
  entry.count++;
  ipRequestCounts.set(ip, entry);
  return entry.count <= KEY_REQUEST_MAX_PER_WINDOW;
}

Key Improvements:

  1. Redis INCR Operation: await redis.incr(key) atomically increments the counter for ratelimit:keys:{ip}. This operation is thread-safe and works correctly even with concurrent requests across multiple instances.

  2. Automatic Expiration: await redis.expire(key, Math.ceil(KEY_REQUEST_WINDOW_MS / 1000)) sets a 60-second TTL on the first request. Redis automatically deletes the key after 60 seconds, implementing the sliding window without manual cleanup.

  3. Graceful Fallback: The original in-memory logic remains as a fallback if Redis is unavailable, ensuring the API doesn't break during Redis outages (though rate limiting would be degraded).

  4. Async/Await Migration: The function signature changed from function checkIpRateLimit(ip) to async function checkIpRateLimit(ip), and the call site at line 116 updated to if (!(await checkIpRateLimit(clientIp))).

This specific change solves the restart problem (Redis persists data to disk), the multi-instance problem (all instances share the same Redis), and provides atomic operations that prevent race conditions.

Key Takeaways

  • The ipRequestCounts Map in checkIpRateLimit() reset on every server restart, allowing attackers to bypass the 10 requests per 60 seconds limit by timing attacks around deployments.

  • Load-balanced deployments multiplied the effective limit by the number of instances, since each maintained separate in-memory counters—3 instances meant 30 requests per minute per IP instead of 10.

  • Redis-backed rate limiting with INCR and EXPIRE operations provides atomic, persistent, distributed counters that survive restarts and work correctly across all server instances.

  • The fix maintains backward compatibility by keeping the in-memory fallback when Redis is unavailable, preventing complete API failure during infrastructure issues.

  • Rate limiting is only as strong as its storage layer—in-memory state is never sufficient for production security controls that must survive restarts and scale horizontally.

How Orbis AppSec Detected This

  • Source: The rate limiting mechanism's reliance on in-memory storage (ipRequestCounts Map) as the single source of truth for security-critical state.
  • Sink: The checkIpRateLimit(ip) function at everclaw-key-api/server.js:50 and its invocation at line 116 in the /api/keys/request POST endpoint.
  • Missing control: Persistent, shared storage backend for rate limit counters; no synchronization mechanism across server instances or restart boundaries.
  • CWE: CWE-770 (Allocation of Resources Without Limits or Throttling)
  • Fix: Migrated rate limiting to Redis using atomic INCR operations with automatic TTL expiration, ensuring counters persist across restarts and synchronize across all server instances.

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 Everclaw Key API rate limiting vulnerability demonstrates a common but dangerous pattern: using in-memory storage for security controls that must be reliable and consistent. While the original implementation correctly tracked request counts and enforced limits, its architectural choice of a JavaScript Map made it trivially bypassable through server restarts or multi-instance deployments.

The Redis-based fix transforms a weak, stateless protection into a robust, distributed security control. This isn't just about preventing API key abuse—it's a blueprint for implementing any rate limiting, throttling, or quota system in modern, horizontally-scaled applications.

Remember: security controls that live only in memory are security controls that can disappear at the worst possible moment. Choose persistence. Choose shared state. Choose Redis.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

critical

Music Studio App Express Server: CWE-287 Authentication Bypass via

A critical authentication bypass vulnerability in a music studio application's Express server allowed remote attackers to access API endpoints despite the intended localhost-only restriction. The localRequest middleware validated the Host header but failed to verify the actual remote socket address, enabling trivial bypasses in containerized and multi-user environments.

high

CVE-2026-104850: MCP TypeScript SDK OAuth Credential Leak

The MCP TypeScript SDK (`@modelcontextprotocol/sdk`) contained a flaw in its OAuth client flow where the MCP server being connected to could influence which authorization server the client talked to, meaning client credentials and token-exchange traffic could be directed to an endpoint the attacker controls. This project was pinned to the affected `1.30.0`; the dependency has been moved to `^1.31.0`, which carries the upstream fix for CVE-2026-104850. Any MCP client that performs OAuth against t

high

SQLite Auth Database Plaintext Storage in InitAuthDB()

The `InitAuthDB()` function opened SQLite databases without restricting filesystem permissions, leaving bcrypt password hashes, session tokens, and user settings exposed to any local account with read access. The fix explicitly sets `os.Chmod(filepath, 0600)` immediately after database creation, ensuring only the owner can access authentication secrets.

high

undici 8.10.0 Cache Poisoning: CVE-2026-85152 Auth Bypass

undici, the HTTP client used by Node.js's `fetch()` implementation, shipped a caching layer that did not properly isolate cached responses by origin, letting a response poisoned on one origin be served to requests for another. This created a path to cross-origin authentication bypass, tracked as CVE-2026-85152 and fixed in undici 8.10.2.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.