学生がMongoDBクエリを作成できる学習プラットフォームが、Node.jsのvmモジュールを廃止し、実行前にすべてのクエリを抽象構文木(AST)にパースするように変更しました。この変更により、突破される可能性があったサンドボックスが取り除かれ、不正な文字列の注入によって悪意のあるコードが実行されることからバックエンドが保護されます。
なぜ元のサンドボックスは失敗したのか
初期の実装では、ライブデータベースハンドルをvm.runInContext呼び出しでラップし、正規表現を使用してユーザーを制限しようとしていました。findやaggregateといったメソッド名のみを許可し、それ以外の識別子はすべてフィルタリングされる仕組みでした。
2つの欠陥が、そのアプローチを安全でないものにしていました:
- 正規表現によるフィルタリングは回避可能です。 JavaScriptでは、ブラケット記法(
obj["constructor"])を使用して任意のプロパティにアクセスできます。攻撃者はFunctionコンストラクタを取得して新しい関数を構築し、任意のコードを実行できます。ソースコードは無数の方法で書き換えられるため、正規表現では根本的なプロパティアクセスを検知できません。 vmはセキュリティ境界ではありません。 Node.jsのドキュメントには、vmはグローバルオブジェクトを分離するものの、プロセス全体を分離するわけではないと記載されています。サンドボックス内にライブデータベース接続を注入することで、コンテキスト内のコードはその接続に対して、データの書き込みや削除を行うメソッドを含む、あらゆるメソッドを呼び出す能力を保持してしまいます。サンドボックスは、コードがホストプロセスに影響を与えることを防げませんでした。
ASTベースのソリューション
チームはコードの実行を静的解析に置き換えました。クエリ文字列は現在acornパーサーに渡され、コードの構文構造を木形式で表現したAST(抽象構文木)が生成されます。ASTは、厳格なホワイトリストに基づいてノードごとに検査されます:
- リテラル、配列、オブジェクトは、プレーンな値として現れる場合にのみ許可されます。
- メソッド呼び出しは、定義済みのセット(
find、sort、limitなど)に限定されます。それ以外の呼び出しはすべて拒否されます。 - 計算されたプロパティアクセス(例:
obj[expr])や、明示的にリストされていないノードタイプは、即座にエラーとなります。
パーサーは生のテキストではなくツリーに対して動作するため、別の書き方やブラケット記法のトリックに騙されることはありません。正規表現をすり抜けていたであろうコンストラクタチェーンも、認識できないノードとして現れ、コードが実行される前に拒否されます。
セキュリティにおける意味
新しい設計は「デフォルト拒否(deny by default)」の哲学に従っています:
- 禁止事項ではなく、許可事項を定義する。 テキストベースの許可リスト(allow-listing)を網羅することは不可能ですが、ASTにはノードタイプの有限のセットがあるため、網羅的なチェックが可能です。
- サンドボックス内にライブリソースを絶対に公開しない。 隔離されたコンテキストにデータベースハンドルを渡すと、サンドボックス化されたコードがバックエンドへの直接的な経路を得てしまいます。パーサーによるアプローチでは、ユーザーコードにライブオブジェクトを渡すことは決してなく、クエリの意図のみを抽出します。
- 構造を検証してから、安全に実行する。 ASTが検証を通過すると、プラットフォームは独自の信頼できるコードパスを使用して、許可された呼び出しを実際のMongoDBドライバーのメソッドに変換します。
次に注意すべき点
- コードベース内の
eval、new Function、またはvmの使用を監査する。 ホワイトリストであっても、JavaScriptの動的な性質によって覆される可能性があります。 - 可能な限り、ユーザー生成コードに対してASTパースを採用する。
acorn、esprima、またはbabel-parserなどのライブラリを使用すれば、変換は簡単に行えます。 - ライブオブジェクトの露出を制限する。 データベース接続、ファイルハンドル、またはネットワークソケットにアクセスする必要がある場合は、許可する最小限のメソッドのみを公開するプロキシでラップしてください。
- エッジケースのテストを自動化する。 ブラケット記法、計算されたキー、またはプロトタイプ操作を使用するクエリを生成し、パーサーがそれらを拒否することを確認してください。
まとめ
サンドボックスとしてvm.runInContextに頼ることは、誤った安心感を与えます。正規表現によるフィルタリングではJavaScriptの柔軟な構文をカバーできず、サンドボックスはプロセスのリソースを分離しません。ユーザー入力をASTにパースし、理解可能なノードのみをホワイトリスト化することで、悪意のあるコードが実行される前に阻止する、具体的でメンテナンス可能な障壁を構築できます。ユーザーがコードを書けるプラットフォームを運営しているなら、今すぐevalスタイルの実行を静的解析に置き換えましょう。
