GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-R2PF-9CW4-5J65

Vulnerability from github – Published: 2026-09-16 16:19 – Updated: 2026-09-16 16:19
VLAI
Summary
node-opcua: TCP Socket Leak (FIN-WAIT-2) via keepalive reconnection cycle - Resource Exhaustion
Details

SUMMARY

A combination of bugs in node-opcua causes unlimited TCP socket accumulation (FIN-WAIT-2 state) during automatic reconnection, leading to memory exhaustion and eventual container/process crash (OOM kill). The issue is triggered by the default configuration (keepSessionAlive: true) when the OPC UA server has clock skew relative to the client.

Affected version: Tested on 2.169.0 (latest as of April 2026).

ENVIRONMENT

  • Node.js: v24.11.0
  • node-opcua: 2.169.0
  • OS: Linux (containerized via Podman, slirp4netns networking)
  • OPC UA Server: Industrial PLC (opc.tcp endpoint), clock skew of ~50 minutes ahead of client
  • Client config: keepSessionAlive: true (default), keepAliveInterval: 3000, securityMode: None, securityPolicy: None

ROOT CAUSE ANALYSIS

Bug #1 - ClientTCP_transport._on_ACK_response() uses socket.end() instead of socket.destroy()

File: node-opcua-transport/src/client_tcp_transport.ts, _on_ACK_response() method

When the HEL/ACK handshake fails during a reconnection attempt, the error handler calls socket.end():

if (err || !data) {
    externalCallback(err || new Error("no data"));
    if (this._socket) {
        this._socket.end();   // <- sends TCP FIN, leaves socket in FIN-WAIT-2
    }
}

socket.end() sends a TCP FIN and waits for the peer to close its side. If the peer doesn't respond (common with PLCs), the socket remains in FIN-WAIT-2 state indefinitely, leaking file descriptors and memory. During rapid reconnection cycles (triggered by Bug #2 below), every failed HEL/ACK creates a new leaked socket.


Bug #2 - ClientSessionKeepAliveManager._ping_server() treats BadInvalidTimestamp as network outage

File: node-opcua-client/src/client_session_keepalive_manager.ts, _ping_server() method

The keepalive manager reads Server.ServerStatus.CurrentTime on each ping cycle. If the server responds with BadInvalidTimestamp (because the client's RequestHeader.timestamp falls outside the server's tolerance window due to clock skew), the manager treats this as a fatal network error:

// Any error -> emit("failure") -> terminateConnection() -> forceConnectionBreak()

This triggers a full transport-level reconnection on every keepalive cycle (every keepAliveInterval ms). Combined with Bug #1, each reconnection attempt leaks one TCP socket in FIN-WAIT-2. Impact amplification: With keepAliveInterval: 3000 (3 seconds), the client leaks ~20 sockets/minute, ~1200/hour, exhausting resources in hours.

REPRODUCTION STEPS

  1. Set up an OPC UA server with a clock skewed more than the server's timestamp tolerance ahead of the client.
  2. Connect using node-opcua with default settings (keepSessionAlive: true).
  3. Monitor TCP sockets: ss -antp | grep FIN-WAIT-2 | wc -l
  4. Observe FIN-WAIT-2 count growing continuously (approximately one per keepalive interval).
  5. Eventually the process runs out of file descriptors or memory and crashes.

SUGGESTED FIXES

For Bug #1 (_on_ACK_response):

// Replace socket.end() with socket.destroy()
if (this._socket) {
    this._socket.destroy();
}

For Bug #2 (_ping_server): Distinguish between transport-level errors (actual network outage) and application-level OPC UA status codes like BadInvalidTimestamp. The latter indicates the server is reachable and the session is alive - only the timestamp validation failed. The keepalive should not trigger reconnection.

REPORTER

Marco Velluso @Velluso velluso.marco64@gmail.com Requesting CVE assignment and credit as reporter upon fix publication.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "node-opcua-transport"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.170.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "node-opcua-client"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.170.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "node-opcua"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.170.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-68904"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-400"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-16T16:19:21Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "SUMMARY\n-------\nA combination of bugs in node-opcua causes unlimited TCP socket accumulation (FIN-WAIT-2 state) during automatic reconnection, leading to memory exhaustion and eventual container/process crash (OOM kill). The issue is triggered by the default configuration (keepSessionAlive: true) when the OPC UA server has clock skew relative to the client.\n\nAffected version: Tested on 2.169.0 (latest as of April 2026).\n\nENVIRONMENT\n-----------\n- Node.js: v24.11.0\n- node-opcua: 2.169.0\n- OS: Linux (containerized via Podman, slirp4netns networking)\n- OPC UA Server: Industrial PLC (opc.tcp endpoint), clock skew of ~50 minutes ahead of client\n- Client config: keepSessionAlive: true (default), keepAliveInterval: 3000, securityMode: None, securityPolicy: None\n\nROOT CAUSE ANALYSIS\n-------------------\n\nBug #1 - ClientTCP_transport._on_ACK_response() uses socket.end() instead of socket.destroy()\n\nFile: node-opcua-transport/src/client_tcp_transport.ts, _on_ACK_response() method\n\nWhen the HEL/ACK handshake fails during a reconnection attempt, the error handler calls socket.end():\n\n    if (err || !data) {\n        externalCallback(err || new Error(\"no data\"));\n        if (this._socket) {\n            this._socket.end();   // \u003c- sends TCP FIN, leaves socket in FIN-WAIT-2\n        }\n    }\n\nsocket.end() sends a TCP FIN and waits for the peer to close its side. If the peer doesn\u0027t respond (common with PLCs), the socket remains in FIN-WAIT-2 state indefinitely, leaking file descriptors and memory.\nDuring rapid reconnection cycles (triggered by Bug #2 below), every failed HEL/ACK creates a new leaked socket.\n\n---\n\nBug #2 - ClientSessionKeepAliveManager._ping_server() treats BadInvalidTimestamp as network outage\n\nFile: node-opcua-client/src/client_session_keepalive_manager.ts, _ping_server() method\n\nThe keepalive manager reads Server.ServerStatus.CurrentTime on each ping cycle. If the server responds with BadInvalidTimestamp (because the client\u0027s RequestHeader.timestamp falls outside the server\u0027s tolerance window due to clock skew), the manager treats this as a fatal network error:\n\n    // Any error -\u003e emit(\"failure\") -\u003e terminateConnection() -\u003e forceConnectionBreak()\n\nThis triggers a full transport-level reconnection on every keepalive cycle (every keepAliveInterval ms). Combined with Bug #1, each reconnection attempt leaks one TCP socket in FIN-WAIT-2.\nImpact amplification: With keepAliveInterval: 3000 (3 seconds), the client leaks ~20 sockets/minute, ~1200/hour, exhausting resources in hours.\n\nREPRODUCTION STEPS\n------------------\n1. Set up an OPC UA server with a clock skewed more than the server\u0027s timestamp tolerance ahead of the client.\n2. Connect using node-opcua with default settings (keepSessionAlive: true).\n3. Monitor TCP sockets: ss -antp | grep FIN-WAIT-2 | wc -l\n4. Observe FIN-WAIT-2 count growing continuously (approximately one per keepalive interval).\n5. Eventually the process runs out of file descriptors or memory and crashes.\n\nSUGGESTED FIXES\n---------------\n\nFor Bug #1 (_on_ACK_response):\n\n    // Replace socket.end() with socket.destroy()\n    if (this._socket) {\n        this._socket.destroy();\n    }\n\nFor Bug #2 (_ping_server):\nDistinguish between transport-level errors (actual network outage) and application-level OPC UA status codes like BadInvalidTimestamp. The latter indicates the server is reachable and the session is alive - only the timestamp validation failed. The keepalive should not trigger reconnection.\n\nREPORTER\n--------\nMarco Velluso @Velluso\n[velluso.marco64@gmail.com](mailto:velluso.marco64@gmail.com)\nRequesting CVE assignment and credit as reporter upon fix publication.",
  "id": "GHSA-r2pf-9cw4-5j65",
  "modified": "2026-09-16T16:19:21Z",
  "published": "2026-09-16T16:19:21Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/security/advisories/GHSA-r2pf-9cw4-5j65"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/pull/1497"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/commit/1959cbb8946b386d2e24a1cce05b7148099d36e7"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/commit/481664fa6ba8204737c5a92797ff68c3ae780c1c"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/commit/4d59197e2dbd82791d7f36dad7da178715e0c27a"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/commit/dc406fd2d364aa69dd173be21ed32a7ff425017a"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/node-opcua/node-opcua"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-opcua/node-opcua/releases/tag/v2.170.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "node-opcua: TCP Socket Leak (FIN-WAIT-2) via keepalive reconnection cycle - Resource Exhaustion"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…