Back to Blog
critical SEVERITY6 min read

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.

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

Answer Summary

Unbounded WebSocket message handling (CWE-770: Allocation of Resources Without Limits or Throttling) in WebSocketCrossServerAdapter.js allowed attackers to overwhelm Redis nodes and WebSocket servers through message flooding. The fix adds a `rateLimit` constructor option (defaulting to 100 messages/second per connection) that silently drops excess messages within each 1-second window, preventing denial-of-service while maintaining backward compatibility for valid use cases.

Vulnerability at a Glance

cweCWE-770 (Allocation of Resources Without Limits or Throttling)
fixAdded configurable `rateLimit` option with 100 msg/sec default per connection
riskDenial of service through message flooding, Redis node exhaustion, WebSocket server resource exhaustion
languageJavaScript/Node.js
root causeNo limits on inbound WebSocket message processing rate per connection
vulnerabilityUnbounded resource consumption / lack of rate limiting

Introduction

In a Node.js WebSocket cross-server adapter library, we discovered a critical resource exhaustion vulnerability in src/WebSocketCrossServerAdapter.js that could allow attackers to degrade or disable distributed WebSocket services through simple message flooding. The WebSocketCrossServerAdapter class handles message routing between WebSocket servers and Redis backends for distributed deployments, but it processed every single inbound message without any rate limiting — creating a perfect denial-of-service vector.

The vulnerability was particularly dangerous because this adapter is designed for horizontal scaling scenarios. An attacker connecting to any single node could generate enough traffic to cascade through the entire Redis cluster, potentially affecting all connected WebSocket servers and their clients. With no rateLimit option in the constructor and no internal throttling mechanism, the code at line 1 and throughout the message handling path accepted unlimited messages from any authenticated (or even unauthenticated) WebSocket connection.

The Vulnerability Explained

The core issue was architectural: the WebSocketCrossServerAdapter class implemented message listeners without any ingestion controls. Here's what the vulnerable message handling pattern looked like conceptually:

// VULNERABLE: No rate limiting on message processing
class WebSocketCrossServerAdapter {
  constructor(options) {
    // Missing: rateLimit configuration
    this.setupWebSocketHandlers();
  }

  setupWebSocketHandlers() {
    this.wss.on('connection', (ws) => {
      ws.on('message', (data) => {
        // EVERY message processed immediately, no throttling
        this.processMessage(data);      // CPU work
        this.publishToRedis(data);       // Network I/O to Redis
        this.broadcastToRoom(data);      // Fan-out to other clients
      });
    });
  }
}

The specific attack path:

  1. Attacker connects to any WebSocket endpoint using WebSocketCrossServerAdapter
  2. Floods messages — the attacker sends 10,000+ messages per second (easily achievable with a simple script)
  3. Each message triggers:
    - JSON parsing and validation (CPU)
    - Redis PUBLISH command (network I/O, Redis CPU)
    - Potential broadcast to room members (multiplied fan-out)
  4. Resource exhaustion — Redis nodes become saturated, WebSocket server event loop blocks, memory grows unbounded with pending operations
  5. Cascading failure — other legitimate connections timeout, cluster rebalancing fails, service degrades across all nodes

The vulnerability was especially insidious because heartbeat messages counted toward the flood. Many WebSocket implementations send frequent pings; a malicious client could exploit this by sending artificial "heartbeat-like" traffic that the server couldn't distinguish from legitimate keepalive traffic.

The Fix

The fix introduces a configurable per-connection rate limit with sensible defaults. Here's the change implemented across the codebase:

Documentation Updates (README.md, README.zh-CN.md, api.en-US.md)

The fix begins with documentation transparency — users need to know this protection exists:

+ Built-in per-connection inbound message rate limiting (`rateLimit` option) to guard against message-flooding clients

And the detailed API documentation in api.en-US.md:

- `rateLimit` `{number}`: (Optional) Maximum number of inbound WebSocket messages accepted per socket, per second. Default is `100`.

  **Description**: Limits how many messages — including heartbeat messages — a single WebSocket connection may send within a 1-second window. Excess messages are silently dropped for the remainder of that window; the counter resets automatically once the window elapses.  
  This is a per-connection ingress safeguard only — it does **not** limit Redis publishes triggered by business-message listeners (e.g. via `broadcastToRoom`, `broadcast`, `emitCrossServer`). Redis-level publish throttling is not covered by this option.  
  Set to `0` to disable rate limiting entirely.

  ⚠️ **Behavior change**: This option defaults to `100`.

Implementation in WebSocketCrossServerAdapter.js

The actual implementation adds rate limiting logic to the message handler:

// SECURE: With per-connection rate limiting
class WebSocketCrossServerAdapter {
  constructor(options) {
    // NEW: rateLimit configuration with safe default
    this.rateLimit = options.rateLimit !== undefined ? options.rateLimit : 100;
    this.setupWebSocketHandlers();
  }

  setupWebSocketHandlers() {
    this.wss.on('connection', (ws) => {
      // NEW: Per-connection rate limit state
      const connectionState = {
        messageCount: 0,
        windowStart: Date.now()
      };

      ws.on('message', (data) => {
        // NEW: Rate limit check
        if (this.rateLimit > 0) {
          const now = Date.now();
          if (now - connectionState.windowStart >= 1000) {
            // Reset window
            connectionState.windowStart = now;
            connectionState.messageCount = 0;
          }

          if (connectionState.messageCount >= this.rateLimit) {
            // Silently drop excess messages
            return;
          }
          connectionState.messageCount++;
        }

        // Process message only if within limit
        this.processMessage(data);
        this.publishToRedis(data);
      });
    });
  }
}

Key security improvements:

Aspect Before After
Default protection None 100 msg/sec per connection
Configurability N/A rateLimit option (0 = unlimited)
Scope N/A Per-connection, 1-second sliding window
Excess handling Process all Silently drop, no error amplification
Heartbeat protection None All messages counted (prevents bypass)

The fix uses silent dropping rather than connection termination to prevent error-response amplification attacks — where attackers could trigger additional resource consumption through error handling paths.

Key Takeaways

  • The WebSocketCrossServerAdapter constructor now requires an explicit rateLimit decision — the default of 100 messages/second per connection prevents accidental unbounded consumption while remaining generous for real-time applications
  • Heartbeat messages must count toward rate limits — attackers can exploit any message-type exclusion by wrapping attack traffic in allowed message types
  • Silent dropping prevents error amplification — returning errors to flood attackers creates additional work; dropping excess messages with no response is more efficient
  • Per-connection state isolation prevents cross-user impact — rate limit counters are scoped to individual WebSocket connections, ensuring one attacker cannot exhaust limits for others

How Orbis AppSec Detected This

  • Source: Untrusted data enters through the WebSocket message event listener in src/WebSocketCrossServerAdapter.js, which receives arbitrary client-controlled message payloads
  • Sink: The message handler immediately processes and forwards messages to Redis via publishToRedis() and potentially broadcasts to room members, with no throttling on execution frequency
  • Missing control: No rate limiting, token bucket, or message queue backpressure mechanism existed to constrain resource consumption per connection
  • CWE: CWE-770 (Allocation of Resources Without Limits or Throttling)
  • Fix: Added constructor option rateLimit with 100 msg/sec default, implementing per-connection sliding window counters that silently drop excess messages

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 unbounded message handling in WebSocketCrossServerAdapter.js exemplifies how even mature distributed systems can lack basic resource protections. The fix demonstrates that effective rate limiting doesn't require complex infrastructure — a simple per-connection counter with a 1-second window provides meaningful protection against flooding attacks while preserving the real-time responsiveness that makes WebSocket architectures valuable.

When building scalable WebSocket systems, always assume malicious clients will exploit any unbounded resource. The rateLimit option added here provides that essential boundary, and its default-on behavior ensures protection without requiring developer action.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.