Node.js環境の落とし穴?sm-cryptoのSM2秘密鍵が予測可能となる脆弱性 (GHSA-vh45-f885-3848) を徹底解説
はじめに:Node.js環境における暗号ライブラリの盲点
Webアプリケーション開発において、フロントエンドエンジニアがNode.jsを使用する機会は多岐にわたります。バックエンドAPIの開発、サーバーサイドレンダリング (SSR) 、CLIツール、あるいはElectronアプリケーションなど、Node.jsは現代のWeb開発に不可欠なランタイムです。しかし、その利便性の裏には、ブラウザ環境とは異なる挙動によるセキュリティ上の落とし穴が存在することもあります。
今回解説する脆弱性 GHSA-vh45-f885-3848 は、npmパッケージ「sm-crypto」がNode.js環境でSM2秘密鍵を生成する際に、予測可能な乱数生成器(PRNG)を使用するというCriticalな問題です。この脆弱性は、特に暗号処理を行うアプリケーションにとって、非常に深刻な影響をもたらす可能性があります。
sm-cryptoのSM2秘密鍵、なぜ予測可能に?脆弱性の詳細
sm-cryptoライブラリは、暗号学的な処理のために「jsbn」というライブラリに依存しています。jsbnのSecureRandomクラスは、乱数生成のシード源として、まずWebブラウザ環境であればwindow.crypto.getRandomValuesを利用しようとします。これは暗号学的に安全な乱数生成器(CSPRNG)です。
しかし、Node.js環境では「window」オブジェクトは存在しません。そのため、jsbnはCSPRNGの利用を諦め、代わりにMath.random()とnew Date().getTime()(システム時刻)を組み合わせて乱数シードを生成するフォールバックパスを実行します。これが問題の根源です。
Node.jsにはglobalThis.cryptoを通じてWeb Crypto APIが提供されており、CSPRNGを利用可能です。しかし、jsbnの実装はwindow.cryptoのみをチェックするため、Node.jsの強力なCSPRNGが全く使われず、非暗号学的な乱数源に頼ってしまっていたのです。
V8エンジンのMath.random()はxorshift128+というアルゴリズムに基づいており、数回の出力からその内部状態を復元(予測)することが可能です。さらにシステム時刻も攻撃者が推定可能な情報です。これらの予測可能な値からシードされたARC4ストリーム(jsbnが内部で使用)は、もはや暗号学的な安全性を持ちません。結果として、sm-cryptoのデフォルトAPIであるsm2.generateKeyPairHex()で生成されるSM2秘密鍵や、署名時に使用されるエフェメラルスカラが、攻撃者によって予測可能になってしまいます。
影響と潜在的なリスク:これはCriticalな問題です
この脆弱性は、暗号ライブラリにとって最も深刻な欠陥の一つであり、Critical(最高深刻度)と評価されています。具体的な影響は以下の通りです。
秘密鍵の復元: 攻撃者が、脆弱性のある環境で生成されたSM2秘密鍵のMath.random()の出力(数回)と生成時刻をわずかに推定できれば、その秘密鍵を完全に再現できてしまいます。これは、暗号資産の秘密鍵、認証用の鍵、データの復号鍵など、あらゆる秘密情報を攻撃者に漏洩させるリスクを意味します。
署名の偽造 (Signature Forgery): SM2署名プロセスでは、エフェメラルスカラ「k」という一時的な秘密値が使われます。この「k」が予測可能である場合、単一の署名から秘密鍵が漏洩し、攻撃者は以降の任意のメッセージに対する署名を偽造できるようになります。これにより、認証の迂回や不正なトランザクションの実行が可能になります。
この脆弱性は、事前の認証や特権を必要とせず、sm-cryptoのAPIを通常利用するだけで発生します。影響を受けるのは、sm-crypto 0.4.0(最新リリース)を使用し、jsbn ^1.1.0に依存するNode.js環境です。ブラウザ環境ではwindow.cryptoが使われるため、直接的な影響は受けません。
PoCが示す決定論的な鍵生成
公開されているPoC(Proof of Concept)コードは、この脆弱性の深刻さを明確に示しています。PoCでは、sm-cryptoライブラリがロードされる前に、Math.random()とDate.now()を固定値にフックします。その後、sm2.generateKeyPairHex()を複数回実行すると、毎回全く同じSM2秘密鍵が生成されることが確認できます。
この結果は、秘密鍵が暗号学的に安全なランダム性ではなく、外部から制御可能な(または予測可能な)入力から決定論的に生成されていることの動かぬ証拠となります。攻撃者は、この原理を利用して秘密鍵を再構築できてしまうわけです。
対策と今後の対応
この脆弱性への対応は、セキュリティを維持するために不可欠です。
最優先事項: ライブラリのアップデート: 本脆弱性に対する公式な修正版がリリースされ次第、速やかにsm-cryptoライブラリを最新バージョンにアップデートしてください。これは最も確実で推奨される対策です。
推奨される修正方法: sm-cryptoライブラリ自体が、Node.js環境でcrypto.randomBytesまたはglobalThis.crypto.getRandomValuesを優先的に利用するように修正されるべきです。また、jsbnライブラリもglobalThis.cryptoをチェックするよう改善されることが望ましいとされています。
暫定的な回避策(非推奨、緊急時のみ): sm2.generateKeyPairHex()の引数なしのデフォルト呼び出しを避けることを検討してください。代わりに、Node.jsのcrypto.randomBytes()で生成した強力な乱数を明示的にBigIntegerの引数として渡すことで、CSPRNGを使用するパスを利用できる可能性があります。ただし、これはライブラリの内部実装を深く理解し、正しく適用しないと新たな脆弱性を生むリスクがあるため、非常に慎重な対応が求められます。可能であれば、本脆弱性の修正がなされた代替の暗号ライブラリへの移行も検討すべきです。
フロントエンドエンジニアが学ぶべき教訓
今回の脆弱性は、私たちフロントエンドエンジニアにいくつかの重要な教訓を与えてくれます。
暗号ライブラリの盲信を避ける: 「暗号ライブラリだから安全」と安易に信頼せず、その内部でどのように乱数生成が行われているか、特に異なる実行環境(ブラウザ vs Node.js)での挙動を確認する重要性を示しています。
Node.jsとブラウザの環境差を意識する: JavaScriptのグローバルオブジェクト(window, globalThis)の存在やWeb Crypto APIの利用可能性など、Node.jsとブラウザ環境における差異を常に意識し、コードが意図した通りに動作するかを確認する習慣が重要です。
依存ライブラリのセキュリティチェック: npm auditなどのツールを活用し、依存ライブラリの脆弱性情報を定期的にチェックし、Criticalな脆弱性には迅速に対応できる体制を整えましょう。
セキュリティはレイヤー全体で考える: フロントエンドのコードがNode.jsバックエンドやElectronアプリケーションとして動作する場合、サーバーサイドのセキュリティプラクティスも適用されることを理解し、包括的なセキュリティ対策を講じる必要があります。
まとめ
sm-cryptoにおけるSM2秘密鍵の予測可能性に関する脆弱性 (GHSA-vh45-f885-3848) は、Node.js環境での暗号処理に潜む深刻な問題です。この脆弱性は秘密鍵の漏洩や署名偽造といった重大なリスクを引き起こすため、sm-cryptoを使用している場合は速やかに対応を検討する必要があります。
今回の件を機に、私たちが利用するライブラリの内部動作、特にセキュリティに関わる部分について、より深い理解を持つことの重要性を再認識しましょう。そして、安全なWebアプリケーション開発のために、常に最新のセキュリティ情報にアンテナを張り、適切な対策を講じていきましょう。