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

Prevention & Best Practices

To prevent similar vulnerabilities in your WebSocket implementations:

1. Implement Defense in Depth

Rate limiting at multiple layers:
- Per-connection (as implemented here)
- Per-IP (using middleware like express-rate-limit or ws-rate-limit)
- Global (service-level circuit breakers)

2. Distinguish Message Types Carefully

If your protocol has heartbeat messages, consider:
- Separate rate limits for control vs. data messages
- Token-bucket algorithms for burst tolerance
- Exponential backoff for repeated violations

3. Monitor and Alert

Track these metrics:
- Messages per second per connection (anomaly detection)
- Drop rates due to rate limiting (tuning validation)
- Redis publish latency (downstream impact indicator)

4. Secure Defaults

Always ship with protective defaults:
- Never require opt-in for basic DoS protection
- Document behavior changes in release notes
- Provide escape hatch (rateLimit: 0) for special cases

Relevant Standards

  • CWE-770: Allocation of Resources Without Limits or Throttling
  • CWE-400: Uncontrolled Resource Consumption
  • OWASP WebSocket Security: https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html

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.

References

  • CWE-770: Allocation of Resources Without Limits or Throttling — https://cwe.mitre.org/data/definitions/770.html
  • OWASP WebSocket Security Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html
  • Node.js ws library documentation on rate limiting patterns — https://github.com/websockets/ws/blob/master/doc/ws.md
  • Semgrep rule for unbounded WebSocket message handling — https://semgrep.dev/r?q=javascript.websocket.security.audit.websocket-missing-rate-limit
  • fix: the websocketcrossserveradapter class does not ... in...

Frequently Asked Questions

What is unbounded WebSocket message handling?

A vulnerability where WebSocket servers accept and process unlimited messages from clients without throttling, allowing attackers to exhaust server resources, overwhelm backend services like Redis, or degrade service for legitimate users.

How do you prevent unbounded WebSocket message handling in Node.js?

Implement per-connection rate limiting using token bucket or sliding window algorithms, set reasonable message size limits, and add connection timeouts. The fix here uses a simple counter-based limit with automatic reset per second.

What CWE is unbounded WebSocket message handling?

CWE-770 (Allocation of Resources Without Limits or Throttling) — also related to CWE-400 (Uncontrolled Resource Consumption).

Is Redis clustering enough to prevent this vulnerability?

No. Redis clustering improves availability but doesn't protect against the root cause: unbounded message ingestion at the WebSocket adapter layer. Attackers can still flood individual connections and exhaust application-level resources before Redis even receives the messages.

Can static analysis detect unbounded WebSocket message handling?

Yes. Static analysis can flag missing rate-limiting middleware, unbounded loops in message handlers, and event listeners without throttling. However, dynamic testing and architectural review are often needed to validate actual resource limits.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

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 dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

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 Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) in the `tar` npm package allowed attackers to craft malicious gzip archives that could exhaust memory or CPU during decompression. The fix upgrades `tar` from 7.5.11 to 7.5.21 across `package.json` and `package-lock.json`, closing the resource-exhaustion path without changing any application code.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.