@deno/sandbox という、モジュールの公開がされていました。 こちら、Den Deploy上にサンドボックスを作成し、軽量な Linux microVM でコードを安全に実行できる環境を提供してくれる。
シェルスクリプトの実行
プロセスの生成
JavaScript アプリケーション
REPL の実行
リモートからのファイル操作
などを安全に行うことができる環境を提供してくれる。
今回はこちらを試してみる。
最終的には、JSを書いて、ゴールにたどり着かせるゲームを作った。
こちらで公開中。Deno Sandbox API Test
参考
導入 sandbox は、alpha版として提供されていて使用するには、deploy@deno.com に連絡する必要があった。 しかし、0.2.1からはこの記述が無くなっており、今ならDeno Deployを組織の確認か有料アカウントとして使っていれば使えるようである。 私は、していなかったので次のような画面になった。
どうやら、クレカ他の決済情報により本人確認としているようである。
組織の登録が確認できると、トークンが作成できるようになる。
作成にあたっては、強めの警告を示される。 トークンの有効期限は、24時間から、neverまで選べるが、今回は一旦1週間にしてみた。
トークンが出てくるので、控える。
最初の実行 先の画面にあるような、最小の実行を試す。
まず、.env にトークンを設定。
.env 1 DENO_DEPLOY_TOKEN=<先ほど控えたトークン>
次に、sandboxモジュールを追加する。
1 $ deno add @deno/sandbox
指定通り、ソースを用意。
main.ts 1 2 3 4 5 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();await sandbox.sh `echo "Hello, world!"` ;
そして、実行。最小でもこのくらいのアクセス権が必要になることは認識しておきたい。
1 2 3 4 5 6 7 8 9 $ deno --env .\main.ts deno --env .\main.ts ✅ Granted env access to "WS_NO_BUFFER_UTIL" . ✅ Granted env access to "DENO_DEPLOY_TOKEN" . ✅ Granted env access to "DENO_SANDBOX_BASE_DOMAIN" . ✅ Granted env access to "DENO_DEPLOY_ENDPOINT" . ✅ Granted env access to "DENO_DEPLOY_CONSOLE_ENDPOINT" . ✅ Granted net access to "ams.sandbox-api.deno.net:443" . Hello, world!
以上、linuxのサンドボックス上で、echoが実行され結果を取得できる。
ls.ts 1 2 3 4 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();await sandbox.sh `ls -la /` ;
こちらをすると、本当にVMなのだということがよくわかる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ deno --env .\ls.ts total 7180 drwxr-xr-x 1 root root 80 Nov 27 14:33 . drwxr-xr-x 1 root root 80 Nov 27 14:33 .. -rw-r--r-- 1 root root 0 Nov 17 17:09 .firecracker drwxr-xr-x 1 daemon daemon 4096 Nov 29 06:37 app lrwxrwxrwx 1 root root 7 Nov 17 17:09 bin -> usr/bin drwxrwxrwt 10 root root 200 Nov 29 06:37 data drwxr-xr-x 4 root root 2460 Nov 29 06:37 dev drwxr-xr-x 1 root root 100 Nov 17 17:09 etc -rwxr-xr-x 1 root root 7301040 Nov 27 01:37 fcinit drwxr-xr-x 3 root root 45 Nov 17 17:09 home drwxrwxrwt 2 daemon daemon 40 Nov 27 14:33 isolate lrwxrwxrwx 1 root root 7 Nov 17 17:09 lib -> usr/lib lrwxrwxrwx 1 root root 9 Nov 17 17:09 lib64 -> usr/lib64 drwx------ 2 daemon daemon 3 Nov 26 20:41 mnt drwxr-xr-x 2 root root 27 Nov 17 17:09 overlay dr-xr-xr-x 113 root root 0 Nov 27 14:33 proc drwxr-xr-x 18 root root 384 Nov 27 01:37 rom drwxr-xr-x 2 root root 4096 Nov 27 14:33 root drwxrwxrwt 6 root root 120 Nov 27 14:33 run lrwxrwxrwx 1 root root 8 Nov 17 17:09 sbin -> usr/sbin dr-xr-xr-x 12 root root 0 Nov 27 14:33 sys drwxrwxrwt 9 root root 4096 Nov 29 06:37 tmp drwxr-xr-x 12 root root 194 Sep 8 00:00 usr drwxr-xr-x 5 root root 4096 Nov 27 14:33 var
ディレクトリビューションはDebianのようである。
os.ts 1 2 3 4 5 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();await sandbox.sh `cat /etc/os-release` ;
1 2 3 4 5 6 7 8 9 10 $ deno --env -EN os.ts PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" NAME="Debian GNU/Linux" VERSION_ID="12" VERSION="12 (bookworm)" VERSION_CODENAME=bookworm ID=debian HOME_URL="https://www.debian.org/" SUPPORT_URL="https://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/"
パッケージの一覧をapt list --installed などで取ろうとしたが、空で返ってきた。 おそらくダメと想定しつつ、試しに apt install tree を実行してみた。
apt_install.ts 1 2 3 4 5 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();await sandbox.sh `apt install tree` ;
1 2 3 4 5 6 7 8 9 $ deno --env -EN .\apt_install.ts WARNING: apt does not have a stable CLI interface. Use with caution in scripts. E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied) E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root? error: Uncaught (in promise) SandboxCommandError: Command failed with exit code 100 throw new SandboxCommandError( ^
しっかりブロックされている。
これらを実行した後、Deno Deployのコンソールを見に行くと、実行ログが確認できる。
詳細を見ると、実行されたコマンドの中身まで確認ができる。
sandbox 上で、Deno を動かす 続けて、SandBox 上で、Denoを動かしてみる。
サンプルにあるコードは以下の通り。
any-script-runner1.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();await sandbox.writeTextFile ("hello.ts" , "console.log('Hello, Sandbox!');" );const child = await sandbox.spawn ("deno" , { args : ["run" , "hello.ts" ], stdout : "piped" , }); for await (const chunk of child.stdout ) { console .log (new TextDecoder ().decode (chunk)); }
以下のように実行できる。
1 2 deno --env -EN .\any-script-runner1.ts Hello, Sandbox!
任意のコードを埋めてみる 外部ファイルに書かれたスクリプトをsandbox上で実行してみる。
any-script-runner2.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();const scriptName = `${crypto.randomUUID()} .ts` const scriptFileText = Deno .readTextFileSync ("run-any-script.ts" )await sandbox.writeTextFile (scriptName, scriptFileText);const child = await sandbox.spawn ("deno" , { args : ["run" , scriptName], stdout : "piped" , }); if (child.stdout === null ) { throw new Error ("stdout is null" ); } for await (const chunk of child.stdout ) { console .log (new TextDecoder ().decode (chunk)); }
run-any-script.ts 1 console .log ('Hello, Sandbox!!!!!!' );
1 2 $ deno --env -ENR .\any-script-runner2.ts Hello, Sandbox!!!!!!
これができたので、もらった任意のコードをsandbox上で実行することの確認が取れた。
パーミッションが関わる操作を試みる 任意実行されるコードに、パーミッションが関わる操作を入れてみる。
run-any-script.ts 1 2 3 4 5 6 7 const res = await fetch ("https://www.ccbaxy.xyz/blog/" );console .log (`Response status: ${res.status} ` );const text = await res.text ();console .log (`Response body: ${text.slice(0 , 100 )} ...` );
これを実行すると、何も出力せず終わる状態になったため、改修し出力を追加する。
any-script-runner3.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();const scriptName = `${crypto.randomUUID()} .ts` const scriptFileText = Deno .readTextFileSync ("run-any-script.ts" )await sandbox.writeTextFile (scriptName, scriptFileText);const child = await sandbox.spawn ("deno" , { args : ["run" , scriptName], stdout : "piped" , stderr : "piped" , }); console .log (await child.status )if (child.stdout === null ) { throw new Error ("stdout is null" ); } for await (const chunk of child.stdout ) { console .log (new TextDecoder ().decode (chunk)); } for await (const chunk of child.stderr !) { console .error (new TextDecoder ().decode (chunk)); }
修正後実行すると、sandbox上でのエラーが取得できた。 正しく、sandbox上でもパーミッションが働いていることがわかる。
1 2 3 4 5 6 7 8 9 10 $ deno --env -ENR .\run-any-script3.ts { success: false , code: 1, signal: null } error: Uncaught (in promise) NotCapable: Requires net access to "www.ccbaxy.xyz:443" , run again with the --allow-net flag const res = await fetch("https://www.ccbaxy.xyz/blog/" ); ^ at mainFetch (ext:deno_fetch/26_fetch.js:174:43) at ext:deno_fetch/26_fetch.js:425:11 at new Promise (<anonymous>) at fetch (ext:deno_fetch/26_fetch.js:370:20) at file:///app/d548bdaf-c909-4914-bcbd-6e78f1bdcb0b.ts:2:19
args をargs: ["run", "-N", scriptName] にしてやると、ネットワークアクセスができる。
1 2 3 4 5 6 7 8 9 10 $ deno --env -ENR .\run-any-script3.ts { success: true , code: 0, signal: null } Response status: 200 Response body: <!DOCTYPE html> <html> <head > <meta charset="utf-8" > <!-- Global site tag (gtag.js) - Google An...
sandbox 内でhono を立ち上げてみる READEには、package.jsonとExpressを使った例の記載がある。 ここではdeno.jsonとHonoを使い動作確認を試みる。
create-hono-server.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { Sandbox } from "@deno/sandbox" ;import {delay} from "@std/async" await using sandbox = await Sandbox .create ();const DENO_JSON = { "imports" : { "@hono/hono" : "jsr:@hono/hono@^4.10.7" , } } const SERVER_TS = `import { Hono } from "@hono/hono"; const app = new Hono(); app.get("/", (c) => { return c.text("Hello, Hono! from Deno Sandbox"); }); Deno.serve({ port: 3000 }, app.fetch); ` ;sandbox.writeTextFile ("deno.json" , JSON .stringify (DENO_JSON , null , 2 )); await sandbox.sh `deno install` ;sandbox.writeTextFile ("server.ts" , SERVER_TS ); await sandbox.createJsRuntime ({entrypoint : "server.ts" })const publicUrl = await sandbox.exposeHttp ({port :3000 })console .log (`Hono server is running at: ${publicUrl} ` );const resp = await fetch (publicUrl);console .log (await resp.text ());await delay (30000 );
以下のように、実行するとインストールから始まり、動作確認ができる。 サーバーを作ると、URLが発行されるので、そこに対してfetchを行いレスポンスを確認している。 URLをしばらく有効なように30秒delayをかけ、手元ブラウザからアクセスできるようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ deno --env -ENR .\create-hono-server.ts Download https://jsr.io/@hono/hono/meta.json <省略> Download https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz Installed 1 package in 684ms Reused 0 packages from cache Downloaded 1 package from JSR + Downloaded 4 packages from npm ++++ Dependencies: + jsr:@hono/hono 4.10.7 Hono server is running at: https://577346cc8c854f34b945bcf442a1b818.sandbox.deno.net Hello, Hono! from Deno Sandbox
動的に作成・発行されたURLにアクセスして、応答を取ることができた。 パッケージインストールもできる。
vscode sandbox でVScodeを動かすサンプルがある。
1 2 3 4 5 6 7 8 import { Sandbox } from "@deno/sandbox" ;await using sandbox = await Sandbox .create ();const vscode = await sandbox.vscode ();console .log (vscode.url );await vscode.status ;
このようにすると、URL発行されアクセスするとVSCodeが開く。 設置されたファイルを見るのに便利である。 ファイルの作成、編集もできるが、–watch での監視はうまく動作しないようだった。 セキュリティ的にどうなっているのか、コントロールできるのかは不明。 少なくとも、Deno Deployのコンソールをログアウトしてもアクセスできたので、この点でのコントロールは効いていない。
以下は、ファイルの変更を監視しつつ、Denoサーバーを動かし、VSCodeで編集を目指したがうまくいかなかった例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import { Sandbox } from "@deno/sandbox" ;import {delay} from "@std/async" await using sandbox = await Sandbox .create ();const DENO_JSON = { "imports" : { "@hono/hono" : "jsr:@hono/hono@^4.10.7" , } } const SERVER_TS = `import { Hono } from "@hono/hono"; const app = new Hono(); app.get("/", (c) => { return c.text("Hello, Hono! from Deno Sandbox"); }); Deno.serve({ port: 3000 }, app.fetch); ` ;sandbox.writeTextFile ("deno.json" , JSON .stringify (DENO_JSON , null , 2 )); await sandbox.sh `deno install` ;sandbox.writeTextFile ("server.ts" , SERVER_TS ); await sandbox.createJsRuntime ({entrypoint : "server.ts" })const child = await sandbox.spawn ("deno" , { args : ["run" , "-N" , "--watch" , "server.ts" ], stdout : "piped" , }); const publicUrl = await sandbox.exposeHttp ({port :3000 })console .log (`Hono server is running at: ${publicUrl} ` );const vscode = await sandbox.vscode ();console .log (`VSCode at: ${vscode.url} ` );while (true ) { await delay (10000 ); await sandbox.sh `ls -la` ; await sandbox.sh `cat server.ts` ; }
試しにサンプルアプリ 試しに、ユーザーが任意入力したソースコードを組み込み実行結果を反映するしたゲームを作ってみる。
ここで中身の説明は、荒いので要点のみ。 中身が気になる場合Octo8080X/deno-sandbox-api-test を見て欲しい。
リクエストからユーザーコードを受け取り、使用するライブラリともいえる部分の中に組み込む。 組み込んだコードをsandbox上で実行し、結果を取得し返すという流れ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import { define } from "../../utils.ts" ;import { Sandbox } from "@deno/sandbox" ;function createSrcCode (userScript: string ) { return ` const movePlan: string[] = []; function moveRight() { movePlan.push("right"); } function moveLeft() { movePlan.push("left"); } function moveUp() { movePlan.push("up"); } function moveDown() { movePlan.push("down"); } // User Script Start ${userScript} // User Script End console.log(JSON.stringify({movePlan})); ` ;} async function simulateSandbox (userScript: string ) { await using sandbox = await Sandbox .create (); const scriptName = `${crypto.randomUUID()} .ts` ; console .log (createSrcCode (userScript)); await sandbox.writeTextFile (scriptName, createSrcCode (userScript)); const child = await sandbox.spawn ("deno" , { args : ["run" , scriptName], stdout : "piped" , stderr : "piped" , signal : AbortSignal .timeout (5000 ), }); console .log (await child.status ); if (child.stdout === null ) { throw new Error ("stdout is null" ); } const stdoutTexts = []; for await (const chunk of child.stdout ) { stdoutTexts.push (new TextDecoder ().decode (chunk)); } const stderrTexts = []; if (child.stderr ) { for await (const chunk of child.stderr ) { stderrTexts.push (new TextDecoder ().decode (chunk)); } } if (stderrTexts.length > 0 ) { console .error ("Sandbox stderr:" , stderrTexts.join ("" )); } return { stdout : stdoutTexts.join ("" ), stderr : stderrTexts.join ("" ), status : (await child.status ).code == 0 ? "success" : "error" , } } export const handler = define.handlers ({ async POST (ctx ) { const userScript = await ctx.req .json (); console .log (userScript); const simulateResult = await simulateSandbox (userScript.code ); console .log ("Simulation result:" , simulateResult); return Response .json (simulateResult); }, });
フロントエンドは、monaco-editorを使いコード入力し、結果はbabylon.jsでゲームとして表示した。
Deno sandbox apiを使ってみた。
最近、コードないし疑似コードを書いて遊ぶゲームが流行っていたりと、任意のコードを実行するというニーズがあると感じる。 もちろんAI周りのこともあるわけだが、ハードな利用もライトな利用でも安全にコードを実行するのは、需要がある。 sandboxは有用であると感じた。
とりあえずでは、ゲームを作る感じで使うことになりそうである。
では。