HN公開: WebAssembly QuickJSサンドボックスでJavaScriptを実行
(github.com/sebastianwessel)@sebastianwessel/quickjsは、QuickJSエンジンをWebAssemblyにコンパイルしたサンドボックス内で JavaScriptとTypeScriptコード を安全に実行するためのTypeScriptパッケージ- 信頼できないコードを隔離して実行することに重点を置いており、QuickJS WebAssemblyサンドボックス を通じて軽量で高速な実行環境を提供
- 標準のNode.jsモジュール対応、仮想ファイルシステム のマウント、カスタムNodeモジュールのマウント、
fetchクライアントの提供、テストランナーとChaiベースのexpectを含む - 使用例では
loadQuickJs()でQuickJS wasmを一度読み込んだあと、runSandboxed()とevalCode()でコードを実行し、allowFetch、allowFs、envなどのオプションを渡す - TypeScriptアプリケーションで外部JavaScriptコードを実行する必要がある開発者は、隔離、性能、APIのシンプルさをあわせて考慮した実行環境として活用できる
QuickJS WebAssemblyサンドボックスパッケージ
@sebastianwessel/quickjsは、JavaScriptとTypeScriptコード をWebAssemblyサンドボックス内で実行できるようにするTypeScriptパッケージ- QuickJSエンジンをWebAssemblyにコンパイルして使用し、信頼できないコードを 隔離された環境 で実行する用途に適している
- このパッケージは、軽量で高速なQuickJSエンジンを活用して、コード実行のための堅牢な環境を提供
- 完全なドキュメント、サンプル、オンラインプレイグラウンドを提供
提供機能
- 安全な実行: 信頼できないJavaScriptおよびTypeScriptコードを、安全で隔離された環境で実行
- 標準Node.jsモジュール: 一般的なユースケース向けに、基本的な標準Node.jsモジュールのサポートを提供
- ファイルシステム: 仮想ファイルシステムをマウント可能
- カスタムNodeモジュール: ユーザー定義のNodeモジュールをマウント可能
- ネットワーク呼び出し:
fetchクライアントを提供し、http(s)呼び出しを実行可能 - テスト対応: テストランナーとChaiベースの
expectを含む - 性能と統合性: QuickJSエンジンの軽量性と効率性を活かし、既存のTypeScriptプロジェクトに容易に統合できる
- シンプルなAPI: サンドボックス内でJavaScriptとTypeScriptコードを実行・管理するためのユーザーフレンドリーなAPIを提供
基本的な利用フロー
- まず
@jitl/quickjs-ng-wasmfile-release-syncからvariantを取得し、@sebastianwessel/quickjsからloadQuickJsとSandboxOptionsを使用する loadQuickJs(variant)はQuickJS wasmを読み込んで初期化するが、この処理は リソース集約的 であるため、可能であれば一度だけ実行する方法が推奨されるloadQuickJs()はrunSandboxedを返し、その後のサンドボックス実行に使われるSandboxOptionsの例には次のオプションが含まれるallowFetch: true: コードにfetchを注入し、データ取得を許可allowFs: true: 仮想ファイルシステムをマウントし、node:fsモジュールを提供env: サンドボックスコード内で使用する環境変数の値を渡す
実行例で可能な動作
- サンドボックス内のコードは
pathからjoinをimportし、join('src','dist')を呼び出してホストシステムのログにsrc/distを出力 env.MY_ENV_VARを読み取り、ホストシステムのログに"env var value"を出力new URL('https://example.com')でURLを作成し、fetch(url)を呼び出したあとf.text()の結果を返す- コードは
runSandboxed(async ({ evalCode }) => evalCode(code), options)の形で実行 - 結果の例は
{ ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' }の形式
ライセンスと適した用途
- このプロジェクトは MIT License で提供される
- TypeScriptアプリケーション内でJavaScriptコードを安全に実行したい開発者に適している
- QuickJS WebAssemblyサンドボックスを通じて、性能と安全性 を両立する実行環境を提供
1件のコメント
Hacker News の意見
quickjs-emscripten ランタイムライブラリの作者として、quickjs-emscripten 向けに作られた、人間工学的な標準ライブラリ風の設計が気に入っている。
ブラウザやバンドラーで動かしてみたのか気になる。バリアント名を文字列として受け取り、
import(variantName)に動的に渡す方式は、Webpack のようなツールとは相性がよくない可能性がある。セキュリティ警告: このライブラリは、ゲスト側の信頼できないコードが、ホストの
fetch関数と同じ Cookie でfetchを呼び出せるようにする。fetchを有効にして信頼できないコードを実行してはいけない。サンドボックス内のコードが、ホストコンテキストで認証された任意の HTTP API を呼び出せるなら、それは「サンドボックス」ではない。
quickjs-emscripten が低レベルで、魔法のような機能を避けている理由は、提供する API が安全だと自信を持って言うためだ。魔法のようなシリアライズや、簡単なネットワーク/ファイルシステムアクセス機能の要望はおおむね断っている。こうしたコードはセキュリティ上のミスが起きやすい領域だからだ。
信頼できないコードを実行するときは、サンドボックス自体だけでなく、サンドボックスに公開する API のために書いたコードまで慎重に監査する必要がある。
別の HN ユーザーが Fetch の Cookie について質問したコメントを見て、潜在的なセキュリティ問題に気づいた。
参考資料: Figma プラグインのサンドボックスセキュリティに関する記事 https://www.figma.com/blog/how-we-built-the-figma-plugin-sys...、https://www.figma.com/blog/an-update-on-plugin-security/、Quickjs-emscripten README https://github.com/justjake/quickjs-emscripten
サーバー側とブラウザ側のどちらでも JavaScript をサンドボックス化する方法は多いが、DOM アクセスを「サンドボックス化」する方法があるのかは分からない。
例えば、信頼できないサードパーティには、あらかじめ決めた位置の DOM 要素だけへのアクセスを与える、といった形だ。自分の知る限り、これを可能にする唯一の技術は iframe だが、残念ながら重くて遅い。
プラグインをホストできるアプリを作っているのだが、プラグインに DOM アクセスを与えると、文字どおり何でもできるようになってしまうように思える。
その安全な評価関数はかなりすごい。核心はおそらくここだと思う: https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
プラットフォームレベルでこの問題を解くための分離された Web Componentsというアイデアもある: https://github.com/WICG/webcomponents/issues/1002
jsmirrors に着想を得て「dommirrors」のようなものを自分で試すことはできるが、大仕事になる。jsmirrors をそのまま使って、望むことをある程度達成する回避策もあるが、使いやすくはない。
ただし、DOM アクセスを仲介したりシミュレートして与えたりすることは、ほぼ確実に本当に望んでいるものではない。相手に実際に何を許可したいのかを把握し、その動作を可能にするケイパビリティだけを与えるべきだ。
例えば、どこかにボタンを追加させたいなら、親要素というアンカーポイントを渡し、
document.createElementで DOM ノードを作らせるべきだと考えるかもしれない。だが実際に欲しいのは、彼らにdocument.createElementへのアクセス権を与えることではなく、add-button ケイパビリティだ。だからaddButtonを実装すればよい。さらに読む: <https://news.ycombinator.com/item?id=30703531#30706060>
CSP がこの用途のものだと言う人の話は聞かないほうがいい。そうではない。正確に言えば、CSP は本来の目的に対しても設計が悪く、ユーザーに敵対的な代物なので、現在のように実装・拡張されるべきではなかったと思う。これに依存するのは危険だ。
親ページ側で DOM コンテンツのサイズを測定し、一定の範囲内で iframe のサイズを調整できるので、アプリの UI により自然に統合することはできる。
ユーザー間の露出レベルと信頼レベルに応じて、iframe 内でのなりすまし/フィッシングやクリックジャッキングの試みには注意が必要だ。理想的には、フレームが Web リクエストを一切行えないようにロックすべきで、これは画像の読み込みもできないという意味だ。
そうすれば、例えばユーザーが偽のパスワードフォームにパスワードを入力するようだまされたとしても、フレーム外へデータを流出させる方法はない。
iframe がリクエストできるリソースの種類を制限する主な方法は Content-Security-Policy であり、これによってサードパーティ画像やスクリプトなどをすべて無効にできる。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
ほかの sandbox 属性も有効にし、Web カメラのようなプライバシーに敏感な DOM API へのアクセスは無効にすべきだ。
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/if...
https://developer.mozilla.org/en-US/docs/Web/Security/IFrame...
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pe...
こうすれば、許可する HTML 要素と属性をホワイトリストで制限できる。ネイティブ DOM イベントの受信まで許可しようとすると複雑になるが、不可能ではない。
イベント名の文字列と、イベントを受け取る要素 id を受け取る API のようなものが必要で、実際の DOM でそのイベントを検知した後、使用中の JavaScript サンドボックス内へ同じイベントを複製する必要がある。そのサンドボックス内では、前述の API にアクセスできなければならない。
バックエンドで HTML を動的に変更し、id 値がハッシュ列である
divタグを挿入する。さらに HTML を変更して、自分のドメインからサードパーティコードをリクエストするscriptタグを挿入し、追跡用にそのscriptタグの data 属性へハッシュ値を追加する。バックエンドでサードパーティのコードを修正し、
document.とwindow.のすべてのインスタンスをdocument.getElementById(hash_value).に置き換え、すべてのクエリセレクターが#hash_valueで始まるようにするElementプロトタイプの.parentNodeをカスタムプロパティに置き換え、提供されたコンテナの外へ出ようとする試みを検査してブロックする必要があるその後、HTML ドキュメントをブラウザに送る。サードパーティのコードが壊れても構わない。制約はサードパーティに知らせるべきで、サーバーに送る前に自分たちのコードをテストするのは彼らがやるべきこと
重要なのは、コードが動的に提供されたコンテナの外へ出られないようにすることだけ。セキュリティ違反を見つけるため、この部分は定期的にテストする必要がある
実際には動かないかもしれないが、試してみると面白そう
偶然、先週 QuickJS を試してみて、結局 isolated-vm に落ち着いた。どちらもセキュリティ要件は満たしていたが、設定、破棄、
eval実行のオーバーヘッドの面で isolated-vm のほうがはるかに性能が良かった興味深いアプローチだ。ワーカーで隔離し、JavaScript 環境を整理する手法を使う別の JavaScript サンドボックスライブラリ[1]の作者としては、JavaScript を解釈する方式、つまり JavaScript 内の JavaScript や、この場合のような WASM 内の JavaScript が、最も高いレベルの隔離を提供すると考えている
また、ホスト側の JavaScript 仮想マシン自体のバグに直接さらされることもない。Node を対象にするなら、これはより重要かもしれない。最近の一部の流れを除けば、Node.js は Deno と違って 隔離とサンドボックス化 を念頭に置いて設計されたようには見えないからだ
API を見ると、
createRuntimeがfetch以外にホスト環境の呼び出しを定義できるのかは分からない。この機能はかなり有用なはずだ。外部世界との通信をすべて許可するかすべて遮断するかではなく、制御された形で制限できるからだ同様に、ブラウザもサポートしていないように見える。少なくとも esm.sh で簡単に確認した限りではそうだった。これも有用な機能になり得る
オーバーヘッドがどの程度なのか気になるので試してみるつもりだし、先に述べたようにアプローチ自体はかなり堅実に見える
[1] @exact-realty/lot
ホスト関数の公開、ゲスト関数の呼び出し、カスタムモジュールローダーなどのための API もある: https://github.com/justjake/quickjs-emscripten?tab=readme-ov...
newFunctionAPI ドキュメント: https://github.com/justjake/quickjs-emscripten/blob/main/doc...CPU が速くなりすぎたので、今度は インタプリタの中でインタプリタ を動かそうという話に見える
QuickJS で JavaScript を実行する利点として 性能 は挙げない。QuickJS はホスト側の JavaScript 仮想マシンとはまったく勝負にならない
ただし、古い C インタプリタや JavaScript で実装したインタプリタよりは速い可能性がある
これでユーザーが提供した JavaScript コードを実行できそうだ。ユーザーの TypeScript コードをサンドボックス環境でバンドラーにかける方法を探していた
QuickJS 内で webpack のような バンドラー を実行する方法について、おすすめがあるか気になる
[1]: https://webcontainers.io/
とてもクールだ。WASM にコンパイルされているなら、ブラウザで実行 できるのか気になる。可能で、しかもリクエストに Cookie を付けずに
fetchリクエストを送れるなら面白そうだ以前の職場では Quickjs-emscripten で segmentation fault と無言のエラーがあまりに多く、プロジェクトが保留になった
もう一度やるなら、より多くのプログラムで正しく動作し、公式に承認された WASM バンドルがあるエンジンを使うと思う
iframe でコードを安全にサンドボックス化できると思っていたが、確信はない。もちろん独自のインタプリタを使えば、より細かい時間制限やカスタム API といった機能をもっと入れられそうだ
今は window message でコードを受け取る iframe を使っていて、入力コードを評価したあと window message で応答を返せるので、かなりうまく動いている。ただし、サンドボックスの外へ抜け出す穴があるかどうかは確信がない
もっと複雑な場合、たとえばパッケージ依存関係は Babel でパースしてから CDN(esm.sh)を使う import map を作ろうとしたが、CDN 化されたバージョンがうまく動かないことがあった
そこで StackBlitz を使ったところ、かなりうまく動くが、セキュアコンテキストではない環境では少し問題がある
結局、コードと依存関係(package.json)を受け取り、Docker コンテナ内で Vite ビルドを実行して結果を返す小さな Web サーバーを作った。それなりにうまく動くが、ときどき遅い
完全にクライアント側でビルドできるとよく、StackBlitz はおおよそそういうことをしている