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
Published September 5, 2026Reviewed 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.

Prevention & Best Practices

To avoid rate limiting vulnerabilities in your Node.js APIs:

  1. Always Use Shared Storage for Rate Limits: Redis, Memcached, or a database. Never use in-memory Maps, objects, or module-level variables for production rate limiting.

  2. Implement Atomic Operations: Use Redis INCR or database UPDATE...SET count = count + 1 with proper locking. Avoid read-modify-write patterns that create race conditions.

  3. Test Multi-Instance Scenarios: Run your application with multiple instances behind a load balancer in staging. Verify rate limits work correctly across all instances.

  4. Monitor Rate Limit Effectiveness: Log when rate limits trigger. If you never see 429 responses, your limits might be too high or bypassable.

  5. Layer Your Defenses: Combine IP-based limiting with:
    - Account/API key-based limits
    - CAPTCHA for suspicious patterns
    - Exponential backoff for repeated violations
    - Web Application Firewall (WAF) rules

  6. Use Battle-Tested Libraries: Consider express-rate-limit with Redis store, rate-limiter-flexible, or cloud provider solutions (AWS API Gateway throttling, Cloudflare Rate Limiting).

  7. Configure Proxy Trust Correctly: If behind a reverse proxy, set app.set('trust proxy', true) in Express so req.ip reflects the real client IP, not the proxy's IP.

Relevant Security Standards:

  • OWASP API Security Top 10: API4:2023 Unrestricted Resource Consumption
  • CWE-770: Allocation of Resources Without Limits or Throttling
  • CWE-307: Improper Restriction of Excessive Authentication Attempts

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.

References

Frequently Asked Questions

What is in-memory rate limiting vulnerability?

It's when rate limit counters are stored in application memory (like a Map or object) rather than persistent shared storage, causing the limits to reset on restart and fail to work across multiple server instances in load-balanced environments.

How do you prevent rate limiting bypass in Node.js?

Use a shared, persistent data store like Redis or Memcached to track rate limit counters. Implement atomic increment operations (Redis INCR) with TTL expiration, and ensure all server instances share the same backend storage.

What CWE is rate limiting bypass?

CWE-770 (Allocation of Resources Without Limits or Throttling) covers insufficient rate limiting, and CWE-307 (Improper Restriction of Excessive Authentication Attempts) applies when rate limiting protects authentication endpoints.

Is IP-based rate limiting enough to prevent abuse?

No. While IP-based limiting helps, it can be bypassed via distributed attacks, proxy rotation, or IP spoofing. Combine it with persistent storage (not in-memory), account-based limits, CAPTCHA challenges, and proper proxy trust configuration.

Can static analysis detect rate limiting vulnerabilities?

Yes. Advanced static analysis tools can detect in-memory storage patterns for rate limiting (like Map or object usage), lack of persistence layers, and missing synchronization across instances. They flag when critical security controls depend on volatile state.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

high

How Missing Authentication on Sensitive Endpoints Happens in Node.js Express APIs and How to Fix It

Four critical endpoints in the Everclaw Key API — `/bootstrap/challenge`, `/bootstrap`, `/verify-xpost`, and `/forget` — lacked authentication checks, allowing any unauthenticated attacker to request bootstrap funds, claim codes, and even trigger GDPR data deletion. The fix adds `x-admin-secret` header validation to each endpoint, matching the pattern already used on the `/api/stats` route.

critical

How Exposed Debug Endpoints Happen in Express.js and How to Fix It

A critical security vulnerability in `routes.js` exposed a `/test` endpoint in production without any authentication or authorization checks, potentially allowing attackers to gather system information and reconnaissance data. The fix restricts this debugging endpoint to non-production environments only, preventing unauthorized access while preserving development functionality.

critical

How Rate Limiting Vulnerabilities Happen in Node.js OAuth Endpoints and How to Fix Them

A critical resource exhaustion vulnerability was discovered in the OAuth token endpoint at `server/routes/oauth.js`. Without rate limiting, attackers could flood the `/api/oauth/token` endpoint with requests, each triggering expensive bcrypt verification operations that would exhaust server CPU and memory. The fix implements per-IP rate limiting using `express-rate-limit` to cap requests at 20 per 15-minute window.

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.

critical

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

A critical security vulnerability in the `scripts/close-issues.mjs` file exposed API key patterns in documentation and used unsafe `spawnSync` calls to execute curl commands. The fix replaces dangerous process spawning with native `fetch()` API calls and removes sensitive configuration examples from documentation, eliminating both credential exposure and command injection risks.

high

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict