Nov 10, 2026·10 min read·3 visits
An unauthenticated remote attacker can trigger an infinite synchronous loop in Moleculer's TCP transporter packet parser by sending a 6-byte header with a negative or sub-header (< 6) 32-bit signed length field and a valid 1-byte XOR checksum, permanently blocking the Node.js event loop.
CVE-2026-97705 (GHSA-h89v-g948-47vp) is a high-severity unauthenticated Denial of Service (DoS) vulnerability affecting the Moleculer microservices framework for Node.js in versions prior to 0.14.36 and 0.15.2. The vulnerability resides in the internal TCP transporter packet parser (`src/transporters/tcp/parser.js`), which decodes the 4-byte packet length field as a signed 32-bit integer using `Buffer.prototype.readInt32BE()`. An unauthenticated attacker with network access to an exposed TCP transporter port can transmit a single malformed packet containing a negative length value or a positive length value less than the 6-byte header size. Due to how Node.js `Buffer.prototype.slice()` processes negative or sub-header offsets, the parser fails to advance the incoming buffer cursor while remaining inside a synchronous `while (packet.length > 0)` loop. Because Node.js relies on a single-threaded event loop, this infinite loop permanently blocks execution, freezing all services and endpoints within the affected process until manual intervention.
Moleculer is a microservices framework for Node.js that uses pluggable transporters to enable inter-node communication across distributed service brokers. While Moleculer supports external message brokers such as NATS, Redis, RabbitMQ, and Kafka, it also includes a built-in zero-dependency TCP transporter designed for direct peer-to-peer communication between cluster nodes. The TCP transporter listens on a bound TCP socket and uses a custom binary stream parser (src/transporters/tcp/parser.js) extending Node.js stream.Writable to frame incoming raw TCP chunks into discrete microservice control and RPC packets.
In Moleculer versions prior to 0.14.36 and 0.15.2, the TCP packet parser fails to validate the minimum threshold of the 4-byte packet length field extracted from incoming TCP stream frames. Specifically, the parser reads the length header using the signed integer method Buffer.prototype.readInt32BE(1) without verifying that the resulting value is at least the size of the 6-byte packet header. This weakness is classified under CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop').
When an attacker with network access to a Moleculer node's TCP transporter port sends a single crafted packet containing a negative length (such as -2147483648 via 0x80000000) and a matching 1-byte XOR checksum, the stream parser enters an inescapable synchronous while loop. Because the parser executes directly on the main Node.js execution thread, this infinite loop starves the Node.js event loop, rendering the process completely unresponsive to all TCP, HTTP, timer, and inter-process communication events.
The Moleculer TCP transporter implements a lightweight 6-byte binary framing header to delineate packets over raw TCP streams. Byte 0 stores a 1-byte checksum calculated by XORing bytes 1 through 5 (buf[1] ^ buf[2] ^ buf[3] ^ buf[4] ^ buf[5]). Bytes 1 through 4 represent the total packet length (including the 6-byte header) encoded as a 32-bit Big-Endian integer. Byte 5 specifies the packet type identifier (for example, PACKET_PING_ID = 4), and bytes 6 through length contain the serialized message payload.
When data arrives on a socket, Parser.prototype._write concatenates any leftover bytes from previous chunks with the new chunk and enters a while (packet.length > 0) loop. If packet.length is at least 6 bytes, the parser verifies the 1-byte XOR checksum against byte 0. Once the checksum passes, the parser extracts the frame length via const length = packet.readInt32BE(1). Because readInt32BE interprets the 4-byte field as a signed two's complement integer, any frame where the most significant bit of byte 1 is set (0x80 through 0xFF) yields a negative integer between -2147483648 and -1.
Immediately after decoding length, the parser checks whether the current buffer contains the full message using the condition if (packet.length >= length). Since packet.length is a non-negative integer (>= 6), any negative length value satisfies packet.length >= length. The parser then extracts the message payload via packet.slice(6, length)—which returns an empty buffer because the end offset precedes the start offset—emits the empty data event, and attempts to advance the buffer cursor by executing packet = packet.slice(length).
In Node.js and V8, Buffer.prototype.slice(start) treats a negative start index as an offset from the end of the buffer, resolving the effective starting position to Math.max(0, packet.length + start). When length is a large negative integer (or any negative number where |length| >= packet.length), packet.length + length is less than or equal to zero, clamping the start offset to 0. Calling packet.slice(0) returns a new Buffer slice referencing the exact same memory region and byte length as packet. Consequently, packet.length never decreases, and the while (packet.length > 0) loop repeats indefinitely on the exact same bytes.
A related boundary condition occurs when length is positive but smaller than the 6-byte header size (0 <= length < 6). If length is 0, packet.slice(0) immediately triggers the same infinite loop without consuming a single byte. If length is between 1 and 5, packet.slice(6, length) emits an empty buffer while packet.slice(length) advances the stream cursor into the middle of the current 6-byte header, corrupting frame alignment and spinning on zero-length or misaligned slices.
The vulnerable logic is located in src/transporters/tcp/parser.js inside the _write method of the Parser class. Prior to the fix, the code checked that the incoming buffer had at least 6 bytes (packet.length < 6) so that header bytes 0 through 5 could be safely indexed, but performed no validation on the decoded length value itself.
// Vulnerable implementation in src/transporters/tcp/parser.js
while (packet.length > 0) {
if (packet.length < 6) {
this.buf = Buffer.from(packet);
return cb();
}
// CRC validation on the 6-byte header
const crc = packet[1] ^ packet[2] ^ packet[3] ^ packet[4] ^ packet[5];
if (crc !== packet[0]) {
return cb(new Error("CRC error!"));
}
// BUG 1: Reads 4-byte length as signed 32-bit integer (-2147483648 to 2147483647)
const length = packet.readInt32BE(1);
// BUG 2: If length <= 0 (or < 6), packet.length >= length is always true
if (packet.length >= length) {
const msg = packet.slice(6, length);
this.emit("data", msg);
// BUG 3: Negative length <= -packet.length or length == 0 makes slice a no-op
packet = packet.slice(length);
} else {
// Incomplete packet; wait for next chunk
this.buf = Buffer.from(packet);
return cb();
}
}The maintainers addressed the vulnerability in commits 914074e502ae403a0338ad168b45b16d6bcc3e44 (branch 0.15.x) and 168ecbdc245c9a9d1dc4ef9b8a81dbae38ee3063 (branch 0.14.x). The patch defines a constant HEADER_SIZE = 6 and inserts an explicit boundary check directly after packet.readInt32BE(1).
File: src/transporters/tcp/parser.js
@@ -9,6 +9,9 @@
const Writable = require("stream").Writable;
const { resolvePacketType } = require("./constants");
+// Size of the packet header: 1 byte CRC + 4 bytes length + 1 byte type
+const HEADER_SIZE = 6;
+
/**
* TCP packet parser
*/
@@ -45,7 +48,7 @@ class Parser extends Writable {
// Find all messages from the chunk
while (packet.length > 0) {
- if (packet.length < 6) {
+ if (packet.length < HEADER_SIZE) {
// Too short, waiting for the next chunk
this.buf = Buffer.from(packet);
return cb();
@@ -67,6 +70,14 @@ class Parser extends Writable {
const length = packet.readInt32BE(1);
+ // Guard against malformed length fields. A valid packet is always at
+ // least HEADER_SIZE bytes long. A negative value (the length is read as
+ // a signed int32) or a value smaller than the header would make the
+ // `packet.slice(length)` below a no-op, causing an infinite loop.
+ if (length < HEADER_SIZE) {
+ return cb(new Error("Invalid packet length! " + length));
+ }
+
// The chunk contain a message
if (packet.length >= length) {
const msg = packet.slice(6, length);> [!NOTE]
> Assessment of Fix Completeness: Enforcing if (length < HEADER_SIZE) ensures that length is always a positive integer greater than or equal to 6 before reaching if (packet.length >= length). Consequently, whenever packet.length >= length evaluates to true, packet = packet.slice(length) is guaranteed to advance the buffer by at least 6 bytes, strictly decreasing packet.length on every iteration and guaranteeing termination of the while loop. Note that if maxPacketSize is configured on the parser, that upper-bound check executes prior to or alongside the length check, preventing memory exhaustion from excessively large positive lengths.
Exploiting CVE-2026-97705 requires no authentication, cryptographic secrets, or prior knowledge of the target Moleculer cluster topology. The only prerequisite is network reachability to the TCP port bound by a Moleculer node's TCP transporter. While Moleculer nodes can use UDP multicast or static host lists for peer discovery, the TCP listener port itself accepts raw TCP connections from any source IP unless restricted by network-level controls.
To trigger the infinite loop, an attacker connects to the target TCP port and transmits a single payload of at least 6 bytes. The attacker sets bytes 1 through 4 to any negative 32-bit Big-Endian integer (such as 0x80000000, which decodes to -2147483648) or any integer less than 6 (such as 0x00000000). To pass the parser's header integrity check, the attacker computes the bitwise XOR of bytes 1 through 5 and writes the result into byte 0.
const net = require("net");
// Connect to an exposed Moleculer node TCP transporter port
const client = net.connect({ host: "target-host", port: 5000 }, () => {
// Allocate a 10-byte buffer (minimum 6 bytes required to enter header parsing)
const buf = Buffer.alloc(10, 0);
// Write -2147483648 (0x80000000) as a signed 32-bit Big-Endian integer at offset 1
buf.writeInt32BE(-2147483648, 1);
// Set byte 5 to a valid packet type ID (e.g., PACKET_PING_ID = 4)
buf[5] = 4;
// Calculate the 1-byte XOR checksum across bytes 1..5 to pass CRC check
buf[0] = buf[1] ^ buf[2] ^ buf[3] ^ buf[4] ^ buf[5];
// Transmit the 10-byte payload; the remote Node.js event loop hangs immediately
client.write(buf);
});Upon receiving the 10-byte buffer, the target Moleculer node verifies buf[0], reads length = -2147483648, and enters the infinite loop inside Parser.prototype._write. In addition to forever emitting empty data events to attached stream listeners, the process pegs a single CPU core at 100% utilization and ceases processing all other connections.
CVE-2026-97705 is rated 7.5 (High) under CVSS v3.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). The vulnerability does not permit memory corruption, arbitrary code execution, or unauthorized data disclosure (C:N/I:N), as Node.js Buffer.prototype.slice() clamps out-of-bounds indices safely within the bounds of the allocated V8 ArrayBuffer. However, the availability impact is complete (A:H) for the targeted Node.js process.
Because Node.js executes JavaScript on a single thread, blocking the event loop halts the entire runtime environment. Any HTTP API gateways (moleculer-web), WebSocket servers, health check endpoints, database connection pools, or scheduled cron jobs co-hosted within the same Node.js process immediately stop responding. Furthermore, because the frozen node can no longer send or respond to Moleculer heartbeat packets, peer nodes in the microservice cluster will mark the affected node as offline after the heartbeat timeout expires, potentially triggering cascading load failures if an attacker sequentially targets multiple nodes in the cluster.
As of the publication of this report, the Exploit Prediction Scoring System (EPSS) assigns CVE-2026-97705 a score of 0.00350 (0.35% probability of exploitation in the next 30 days, ~28.9th percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
Development and operations teams using Moleculer should upgrade to the patched releases immediately. For projects on the 0.14.x release line, upgrade to Moleculer 0.14.36 or later. For projects on the 0.15.x release line, upgrade to Moleculer 0.15.2 or later.
# For Moleculer 0.14.x deployments
npm install moleculer@^0.14.36
# For Moleculer 0.15.x deployments
npm install moleculer@^0.15.2If immediate package upgrades cannot be deployed in production, administrators should apply strict network segmentation around all Moleculer nodes. The built-in TCP transporter does not implement transport-layer authentication or encryption by default and is intended exclusively for trusted internal cluster networks. Configure host-based firewalls (iptables, nftables) or cloud security groups to restrict incoming traffic on the TCP transporter ports solely to verified cluster node IP addresses.
Alternatively, organizations can migrate inter-node communication away from the built-in TCP transporter to an authenticated message broker transporter such as NATS, Redis, RabbitMQ, or Kafka. Deployments that exclusively use external broker transporters without initializing the TCP transporter do not instantiate src/transporters/tcp/parser.js and are not reachable via this attack vector.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
moleculer moleculerjs | < 0.14.36 | 0.14.36 |
moleculer moleculerjs | >= 0.15.0, < 0.15.2 | 0.15.2 |
| Attribute | Detail |
|---|---|
| CVE / Advisory ID | CVE-2026-97705 / GHSA-h89v-g948-47vp |
| Weakness Class | CWE-835 (Infinite Loop) |
| Attack Vector | Network (Unauthenticated TCP) |
| CVSS v3.1 Score | 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.35% (28.9th Percentile) |
| Impact | Permanent Node.js Event Loop Blocking (DoS) |
| Exploit Status | Proof-of-Concept Available |
| CISA KEV Status | Not Listed |
The program contains an iteration or loop with an exit condition that cannot be reached or satisfied, causing execution to loop indefinitely and consume 100% of available CPU or hang the thread.
CVE-2026-77596 is an authorization bypass vulnerability in the Kubeflow Pipelines API server affecting multi-user deployments prior to version 2.18.0. The HTTP multipart upload handlers prematurely terminate authorization checks and return success when the target namespace parameter is empty or omitted. Because the default configuration allows pipelines without namespaces to function as cluster-wide shared resources, any low-privileged tenant can create shared pipelines or inject arbitrary pipeline versions into existing shared pipelines without undergoing Kubernetes SubjectAccessReview validation. When other tenants execute unpinned recurring runs tracking these shared pipelines, the attacker-supplied workload runs inside the victim tenant's namespace using the victim's service account credentials.
An authorization bypass vulnerability in Velocidex Velociraptor versions prior to 0.77.2 allows authenticated users with low-privilege query permissions to bypass access controls in the sql() VQL plugin. By supplying crafted MySQL connection strings pointing to an attacker-controlled rogue MySQL server with local-infile enabled, an attacker can read arbitrary files from the Velociraptor server host filesystem with the privileges of the server process.
CVE-2026-108262 (GHSA-9hq4-hm3j-jmph) is a path traversal and arbitrary file overwrite vulnerability in the tar archive extraction routine of DDEV versions 1.25.2 and 1.25.3. Resulting from an incomplete fix for CVE-2026-32885, the Untar function in pkg/archive/archive.go validates absolute symbolic link targets by rebasing them against the extraction destination directory, but subsequently creates the symbolic link on disk using the raw, unvalidated absolute path. When subsequent entries in the archive write through the planted symbolic link, the host operating system resolves the link directly to the host filesystem, allowing an attacker to overwrite arbitrary files with the privileges of the user executing DDEV.
CVE-2026-119298 (GHSA-x89h-7h96-v88f) is a file overwrite confirmation bypass vulnerability in schollz/croc prior to version 11.5.2. The receive-side file validation logic in src/croc/croc.go improperly exempted any incoming file whose basename began with the prefix 'croc-stdin-' from interactive overwrite prompts and automatic collision renaming, failing to verify if the transfer session was actually an internal text transfer. An unauthenticated remote peer participating in a transfer session can exploit this flaw to silently overwrite, truncate, or replace matching pre-existing files in the receiver's working directory.
Claude Code prior to version 2.1.251 contains a Time-of-Check to Time-of-Use (TOCTOU) race condition in its internal file manipulation tools (Edit, Write, Read). Because path validation was performed using string-based pathname and symbolic link resolution without binding to the underlying directory inodes or open file descriptors, a process running concurrently inside the macOS Bash sandbox can atomically replace an in-workspace directory with a symbolic link after validation passes. This causes the unsandboxed main process to follow the symlink and overwrite user-writable files outside the permitted workspace boundary.
CVE-2026-88982 (GHSA-2xwh-426q-42c5) is an authorization bypass and data integrity vulnerability affecting Kubeflow Pipelines versions prior to 2.18.0. The flaw resides in the legacy V1 and raw-Argo execution caching mutating admission webhook and the associated cache-server storage backend. In multi-tenant deployments where namespaces share a centralized cache service, execution cache keys were derived solely from container specification fields without incorporating the workload's Kubernetes namespace. Additionally, the cache watcher trusted mutable pod annotations when persisting execution outputs. Consequently, an authenticated user with workload submission privileges in one tenant namespace can poison cached step outputs consumed by other tenants or read cached outputs generated by workloads in separate namespaces.