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

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.

high

PHP session_start() Missing Secure Cookie Flags in lasturl.php

The `session_start()` call in the last URL tracking component failed to set secure session cookie parameters, allowing session hijacking via man-in-the-middle attacks on HTTP connections or XSS exploitation. The fix configures `session_set_cookie_params()` with `httponly`, `secure`, and `samesite` attributes before starting the session.

critical

Tung Tung Tracker Admin API: Missing Authentication in `/api/sync`

The Tung Tung Tracker administrative API endpoints (`/api/sync`, `/api/backfill`, `/api/show`, `/api/overlay/hide`) relied solely on localhost origin and a static `X-Tracker` header for protection, allowing any local process to execute privileged operations. The fix introduces proper session-based authentication and a random IPC secret for desktop-to-server communication.

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.