[解説] node-opcuaにおけるTCPソケットリーク脆弱性(GHSA-r2pf-9cw4-5j65) - フロントエンドエンジニアが知っておくべきNode.jsのリソース枯渇リスク
はじめに:なぜフロントエンドエンジニアがこの脆弱性を知るべきなのか
JavaScript/TypeScriptを用いた開発は、もはやブラウザ上だけにとどまりません。Node.jsの普及により、バックエンドAPI、デスクトップアプリケーション、CLIツール、さらにはIoTデバイスのゲートウェイなど、その活躍の場は広がり続けています。今回解説する脆弱性「GHSA-r2pf-9cw4-5j65 / CVE-2026-68904」は、Node.jsで産業用プロトコルOPC UAを扱うライブラリ「node-opcua」に関するものです。
直接「フロントエンド」と名の付く領域とは異なるかもしれませんが、Node.jsエコシステム全体に関わる問題であり、Node.jsを用いたバックエンドやエッジデバイス開発に携わる可能性のある方、あるいはビルドツールなどで間接的にNode.jsを利用している方にとっては、ライブラリの選定やリソース管理の重要性を再認識する上で非常に重要な情報です。特に、アプリケーションが長期間稼働する環境では、今回のようなリソースリークは深刻な障害に直結します。
脆弱性の概要:無限のTCPソケットリークとリソース枯渇
この脆弱性は、Node.jsのTCPソケットが「FIN-WAIT-2」状態で無限に蓄積されてしまうことにより、最終的にプロセスまたはコンテナのリソース(ファイルディスクリプタ、メモリ)を枯渇させ、OOM (Out Of Memory) Killによる強制終了を引き起こすものです。深刻度は「High」と評価されています。
具体的な発生条件は以下の通りです。
<ul><li>`node-opcua`ライブラリを使用している。</li><li>OPC UAサーバーとクライアントの間で、サーバー側のクロックがクライアントよりも大幅に進んでいる(クロックスキューがある)環境。</li><li>`keepSessionAlive: true` (デフォルト設定) が有効になっている。</li></ul>
これらの条件が揃うと、`node-opcua`クライアントは自動的な再接続サイクルに入り、そのたびにTCPソケットがリークし、システムが不安定化します。
技術的詳細:2つのバグが引き起こす複合的な問題
この脆弱性は、2つの独立したバグが組み合わさることで、その影響が拡大します。
`node-opcua-transport/src/client_tcp_transport.ts` の `_on_ACK_response()` メソッドにおいて、HEL/ACKハンドシェイクが失敗した際に、エラーハンドラが `socket.end()` を呼び出しています。
<code>if (err || !data) { /* ... */ if (this._socket) { this._socket.end(); } }</code>
ここで問題なのは `socket.end()` です。これはTCP FINパケットを送信し、相手からのFIN/ACKを待つ状態(FIN-WAIT-2)に入ります。しかし、相手のサーバー(特に一部のPLCなど)が応答しない場合、このソケットはFIN-WAIT-2状態で indefinitely(無期限)に保持され続けます。これにより、ファイルディスクリプタとメモリが消費され続けます。
**本来あるべき挙動**: 失敗した接続の場合は、相手からの応答を待たずにすぐにリソースを解放するため、`socket.destroy()` を使用すべきです。`socket.destroy()` は強制的にソケットを閉じ、関連するリソースをクリーンアップします。
`node-opcua-client/src/client_session_keepalive_manager.ts` の `_ping_server()` メソッドでは、セッションのkeep-alive機能が実装されています。このpingサイクル中に、OPC UAサーバーがクライアントの `RequestHeader.timestamp` がサーバーの許容範囲外であると判断し、`BadInvalidTimestamp` エラーを返すと、keep-aliveマネージャーはこのエラーを「致命的なネットワークエラー」として扱ってしまいます。
<code>// Any error -> emit("failure") -> terminateConnection() -> forceConnectionBreak()</code>
これにより、実際にはサーバーとの通信が可能な状態であるにもかかわらず、クライアントは「接続が切断された」と判断し、トランスポートレベルでの再接続を強制的にトリガーします。この再接続の試みがバグ1と組み合わさることで、失敗するたびに新しいTCPソケットがFIN-WAIT-2状態としてリークされていきます。
**影響の増幅**: 例えば `keepAliveInterval: 3000` (3秒) の場合、毎3秒ごとに再接続が試みられ、失敗するたびにソケットがリークします。これは1分あたり約20ソケット、1時間あたり約1200ソケットという速度でリソースを消費し、数時間でシステムがリソース枯渇に陥る可能性があります。
再現手順と確認方法
この問題は、以下の手順で再現可能です。
<ol><li>OPC UAサーバーを、クライアントよりも大幅にクロックが進んでいる(サーバーのタイムスタンプ許容範囲を超える)状態でセットアップします。</li><li>`node-opcua` をデフォルト設定(`keepSessionAlive: true`)でクライアントとして接続します。</li><li>以下のコマンドでTCPソケットの状態を監視します。<pre><code>ss -antp | grep FIN-WAIT-2 | wc -l</code></pre></li><li>`FIN-WAIT-2` 状態のソケット数が、`keepalive` インターバルごとに継続的に増加することを確認します。</li><li>最終的に、プロセスがファイルディスクリプタまたはメモリ不足に陥りクラッシュします。</li></ol>
対策と推奨事項
この脆弱性に対する推奨される修正は以下の通りです。
あなたのプロジェクトで `node-opcua` を使用している場合、以下の対応を検討してください。
まとめ
今回の `node-opcua` の脆弱性は、Node.jsアプリケーションが長期間稼働する環境において、いかにリソース管理が重要であるかを示しています。一見すると無害に見える処理の組み合わせが、システム全体を停止させるほどの深刻な影響を及ぼすことがあります。
フロントエンドエンジニアの皆さんも、Node.jsプロジェクトに携わる際には、利用しているライブラリの既知の脆弱性情報に常にアンテナを張り、定期的な依存ライブラリのアップデートと、システムのリソース状況(メモリ、CPU、ファイルディスクリプタ、ネットワークコネクションなど)の監視を怠らないようにしましょう。安全で堅牢なアプリケーション開発のために、この情報がお役に立てば幸いです。