Skip to main content

Node.js VMモジュールを使ったJavaScriptサンドボックスのセキュリティ上の懸念

著者
feature red team blue team

2023年2月22日

0 分で読めます

エンドユーザーが提供する動的なJavaScriptを実行する必要のある製品の開発を任されましたか?Node.js VMモジュールをベースに構築すれば、JavaScriptサンドボックスを実現できると考えるかもしれません。この記事では、それが推奨される方法とはほど遠い理由と、そうした場合のセキュリティ上の影響について解説します。

バックエンド開発のありきたりな日常を揺さぶるプロジェクトが、ときどきあります。API?メッセージキュー?大量の処理や計算要件?いや、そうではありません。検討してほしいバックログのストーリーはこちらです。

As a user,
I want to write and execute my own custom JavaScript code,
So that it can run within the platform and provide me feedback.

これは、ユーザーから受け取った信頼できないコードを実行するという汎用的なユースケースなので、少し曖昧です。しかし、実際に必要となる場面の例はいくつかあります。

  • replitでは、クラウド上でコーディングし、カスタムIDEでコードを実行できます

  • さまざまな「leet code」形式のコーディング練習・面接プラットフォームでは、特定のアルゴリズムを実装したり、テストを作成したりするために、独自のコードを記述できます。

そこで登場するのが、Node.js VMモジュールです。

Node.js VMモジュール

コアモジュールのnode:vmを使うと、開発者は動的に提供されたコードをV8仮想マシンのコンテキスト内でコンパイルして実行できます。これを聞くと、既知のセキュリティリスクであるJavaScript関数のevalを思い浮かべるかもしれません。では、Node.js VMモジュールはより安全なのでしょうか?「仮想マシン」と名が付いています。

実際の例を見てみましょう。

const vm = require("node:vm");

const productExpirationDays = 14;

// This works just fine - it is a custom code that manipulates
// the data in the context and only there.
const userInputCustomJavaScriptCode = "userCustomNickname = 'Johnny Mnemonic';";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

console.log(context.userCustomNickname);
console.log(context.productExpirationDays);
console.log(productExpirationDays);

上記のコードスニペットでは、ユーザーが提供した独自のJavaScriptコード(変数userInputCustomJavaScriptCodeで示される)を、変数vm.runInContext()で指定したコンテキスト内で実行しています。このコードスニペットは正常に実行され、次の結果が出力されます。

Johnny Mnemonic
undefined
14

しかし、攻撃者は、元々割り当てられたものとは別の変数を操作して、このような動的JavaScriptコードを悪用する可能性があります。この例では、ユーザーの有効期限を設定するproductExpirationDaysという変数があります。攻撃者が動的JavaScriptコードの内容を変更して、この値をより大きくしたらどうなるでしょうか?

const userInputCustomJavaScriptCode = "userCustomNickname = 'Johnny Mnemonic'; productExpirationMinutes += 60";

先ほどのユーザー入力の例を、有効期限の日数を延ばそうとするこのコードに置き換えると、次のエラーが表示されます。

evalmachine.<anonymous>:1
userCustomNickname = 'Johnny Mnemonic'; productExpirationDays += 60
                                        ^

ReferenceError: productExpirationDays is not defined
    at evalmachine.<anonymous>:1:41
    at Script.runInContext (node:vm:141:12)
    at Object.runInContext (node:vm:297:6)

エラーの理由は、動的JavaScriptコードが実行される仮想マシンのコンテキスト内にproductExpirationDays変数が定義されておらず、存在もしないためです。

これで、制限されたJavaScriptサンドボックス内でJavaScriptコードを安全に動的実行する方法を見つけたように思えます。

安全でないJavaScriptサンドボックス

Node.js VMモジュールのcreateContext()とvm.runInContext()を使ったコード例は、単純化しすぎていました。残念ながら実際には、悪意のあるユーザー入力は、より賢く、創造的で、効果的な方法でJavaScriptサンドボックスから抜け出そうとします。

攻撃者が安全でないコードを提供し、実行中のアプリケーションにサービス拒否攻撃を仕掛ける方法を見てみましょう。次のNode.jsスクリプトを考えてください。

const vm = require("node:vm");

const userInputCustomJavaScriptCode =
  "userCustomNickname = 'Johnny Mnemonic'; while(true) {}";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

// This will never run:
console.log("Never fear, I is here");

上記のコード例では、ユーザーが入力に無限ループwhile(true) {}を追加しています。このコードはアプリケーションの他の変数を変更しませんが、サービス拒否攻撃を引き起こします。

安全でないJavaScriptサンドボックスのリスクは、リモートコード実行にも及びます。this.constructor.constructorを使うと、JavaScriptのFunctionオブジェクトを参照できます。JavaScript関数は、文字列として受け取ったコードを実行するという特性を持っています。この特性を利用し、即時実行関数(略してIIFE)と組み合わせることで、実行中のNode.jsプロセスの環境変数を出力させます。

const vm = require("node:vm");

const userInputCustomJavaScriptCode =
  "this.constructor.constructor('console.log(process.env)')()";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

console.log("Mess with the best, drop like your envs!");

上記のコードスニペットを実行すると、すべての環境変数を一覧表示するprocess.envの内容が出力されます。

ここまでで、Node.jsのVMモジュールで信頼できないコードを実行した場合、リモートコード実行がどれほど大きな影響を及ぼすか理解できたでしょう。ユーザーが独自のコードを実行できる場合、Node.jsサーバーのランタイムに完全にアクセスでき、プロセスの起動やファイルシステムへのアクセスなどが可能になります。

まとめ

Node.js環境で安全でないJavaScriptサンドボックスを使うと、深刻な影響が生じ、アプリケーション全体が停止する恐れがあります。攻撃者がJavaScriptサンドボックスを悪用し、有害な入力を提供してNode.jsアプリケーションの性能を低下させ、サービス拒否の脆弱性を引き起こす可能性を見てきました。攻撃対象領域はそれだけではありません。リモートコード実行も可能であり、攻撃者はNode.jsサーバー環境で独自のコードを実行して、アプリケーションプラットフォーム全体を危険にさらすことができます。

実際、Node.js VMモジュールを使ったJavaScriptサンドボックスのセキュリティホールは、より大規模なデータ侵害の始まりにすぎません。Node.jsアプリケーションサーバーが1台でも侵害されると、データベースやクラウドサービスのアクセスシークレットなどの機密情報が露出し、攻撃者がさらなる悪用を進めたり、デプロイされたネットワーク内でラテラルムーブメントを行ったりする可能性があります。

そのため、信頼できないJavaScriptコードを実行するための安全なサンドボックスとして、Node.jsのVMモジュールに依存しないことがベストプラクティスです。この点はNode.js APIドキュメントにも明記されています。「node:vmモジュールはセキュリティ機構ではありません。信頼できないコードの実行には使用しないでください。」

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。