high · 7CVE-2026-68904Sep 16, 2026

CVE-2026-68904: node-opcua-transport TCP Socket Leak via Keepalive Reconnection Loop

Rohit Hatagale
AI Security Researcher, SecureLayer7

A bug in node-opcua causes TCP sockets to pile up indefinitely in FIN-WAIT-2 state during automatic reconnection, eventually exhausting file descriptors and crashing the process.

Packagenode-opcua-transport
Ecosystemnpm
Affected>= 2.0.0, < 2.170.0
Fixed in2.170.0
CVE-2026-68904: node-opcua-transport TCP Socket Leak via Keepalive Reconnection Loop

The problem

Two cooperating bugs in node-opcua versions >= 2.0.0, < 2.170.0 combine to leak one TCP socket per keepalive cycle.

Bug 1: In client_tcp_transport.ts, _on_ACK_response() calls socket.end() on HEL/ACK handshake failure. socket.end() sends a TCP FIN and waits for the peer to close its side. Industrial PLCs often never respond, leaving each socket stuck in FIN-WAIT-2 forever, leaking a file descriptor.

Bug 2: In client_session_keepalive_manager.ts, _ping_server() treats any non-Good OPC UA status code, including BadInvalidTimestamp (0x80230000), as a fatal network error. When a server has clock skew relative to the client, the keepalive ping returns BadInvalidTimestamp every interval, triggering a full transport-level reconnection.

Combined with Bug 1, this creates one new leaked socket every keepAliveInterval milliseconds. At the default 3-second interval that is ~1200 leaked sockets per hour, leading to OOM kill or file descriptor exhaustion within hours.

Proof of concept

A working proof-of-concept for CVE-2026-68904 in node-opcua-transport, with the exact payload below.

javascript
// Trigger: connect to an OPC UA server with clock skew > server timestamp tolerance
// using default keepSessionAlive: true. Monitor socket accumulation:
//
//   ss -antp | grep FIN-WAIT-2 | wc -l
//
// Watch the count grow by one every keepAliveInterval ms.

import { OPCUAClient } from "node-opcua";

const client = OPCUAClient.create({
  keepSessionAlive: true,    // default: true -- enables the ping loop
  keepAliveInterval: 3000,   // default: triggers reconnect every 3 s
  securityMode: "None",
  securityPolicy: "None",
  endpointMustExist: false,
});

// Target: OPC UA server whose clock is >~1 minute ahead of the client.
// The server will return BadInvalidTimestamp (0x80230000) on each Read.
// The keepalive manager misclassifies this as a network failure,
// calls terminateConnection() -> forceConnectionBreak() on every tick,
// and _on_ACK_response() leaks a FIN-WAIT-2 socket on every reconnect attempt.
await client.connect("opc.tcp://<skewed-plc-or-server>:4840");
const session = await client.createSession();
// Leave running. FIN-WAIT-2 count climbs continuously.

Bug 1 root cause: socket.end() performs a half-close (sends FIN, waits for peer FIN). If the peer never replies, the OS keeps the socket in FIN-WAIT-2 until the process exits. The fix replaces socket.end() with socket.destroy(), which immediately frees the file descriptor with a RST.

Bug 2 root cause: _ping_server() did not distinguish OPC UA application-level error codes from true transport failures. BadInvalidTimestamp (0x80230000) means the server is reachable and the session is valid but the client's RequestHeader.timestamp was outside the server's acceptance window due to clock skew.

Treating it as a connection failure caused a full reconnection on every cycle. The fix (v2.171.0) checks for BadInvalidTimestamp specifically and treats it as session-alive, suppressing the reconnection.

CWE-400 (Uncontrolled Resource Consumption) applies because there is no cap on how many sockets can accumulate. The two bugs must both be present for the leak to trigger at high rate, but Bug 1 alone can leak sockets in any reconnection scenario where the peer does not close its side.

The fix

Upgrade node-opcua and node-opcua-transport to 2.170.0 or later. Bug 1 (socket.destroy) is fixed in 2.170.0 (commit 1959cbb). Bug 2 (BadInvalidTimestamp keepalive misclassification) is fixed in 2.171.0 (commit 3fe0102). Upgrade to at least 2.171.0 to fully close both issues.

As a short-term workaround, set keepSessionAlive: false to stop the ping loop, accepting that sessions will not be actively maintained.

Reported by Marco Velluso.

References: [1][2][3][4][5][6]

Related research