deno と node の --experimental-permission を書き比べる
node の --experimental-permission と --experimental-network-imports で deno と同じような http(s) の経由のスクリプトを権限付きで動かせるようになる(予定)。
追記:20240904 --experimental-network-imports
は drop されて消えました
node v21.4.0 と Deno v1.38.5 で検証
Node と Deno で両方で実行できるスクリプトを書く。
どちらも read 権限は与えるが write は渡さない。
// check permissions
const IS_NODE = typeof process === 'object';
const IS_DENO = typeof Deno === 'object';
{
if (IS_NODE) {
console.log('Node Permissions', {
write: process.permission.has('fs.write'),
read: process.permission.has('fs.read'),
});
}
if (IS_DENO) {
console.log('Deno Permissions', {
write: (await Deno.permissions.query({ name: 'write' })).state,
read: (await Deno.permissions.query({ name: 'read' })).state,
});
}
}
// with network imports
import { z } from "https://esm.sh/zod@3.22.4";
const schema = z.object({ name: z.string() });
console.log(schema.parse({ name: 'test' }), );
// with read permission
import fs from 'node:fs';
{
const x = fs.readFileSync('scratch.mjs', 'utf8');
console.log("Length", x.length, "bytes");
}
// no write permission
try {
fs.writeFileSync('scratch.mjs', 'console.log("hello");');
} catch (err) {
// deno handler
if (IS_DENO && err instanceof Deno.errors.PermissionDenied) {
console.log('PermissionError:', err.message);
}
// node handler
if (IS_NODE && err instanceof Error && err.code === 'ERR_ACCESS_DENIED') {
console.log('PermissionError:', err.message);
}
}
node
$ node --no-warnings --experimental-network-imports --experimental-permission --allow-fs-read='*' scratch.mjs
Node Permissions { write: false, read: true }
{ name: 'test' }
Length 1412 bytes
PermissionError: Access to this API has been restricted
deno
$ deno run --allow-read --no-prompt scratch.mjs
Deno Permissions { write: "prompt", read: "granted" }
{ name: "test" }
Length 1412 bytes
PermissionError: Requires write access to "scratch.mjs", run again with the --allow-write flag
実行結果の違い
- どちらも esm.sh からモジュールを解決できている
- どちらも write で例外を吐く
- 権限違反でエラーが発生することは同じだが、エラーオブジェクトの形式が違う
- 権限をもっているか確認する方法が Deno.permissions.query と process.permission.has('...')
将来的に権限を考慮した両対応ライブラリを書くなら、権限周りを抽象化したラッパーライブラリを用意する必要がありそう。
比較表
Deno | Node | |
---|---|---|
Read | --allow-read |
--allow-fs-read='*' |
Read path | --allow-read=/tmp |
--allow-fs-read=/tmp |
Write | --allow-write |
--allow-fs-write='*' |
Write path | --allow-read=/tmp |
--allow-fs-read=/tmp |
Run | --allow-run |
--allow-child-process , --allow-worker
|
Network | --allow-net |
❌ |
Env | --allow-env |
❌ |
- 公式ドキュメントを読み比べてるだけで内部的に実装されてる可能性はある
-
--allow-net
は https://nodejs.org/api/permissions.html#policies で似たようなことができるので、それでオミットされてる可能性がある
私見
権限の抽象化で似たようなラッパーライブラリを用意することは可能だが、プロセスモデルが違うので細かい差異を踏むのが怖い。
サプライチェーンアタックで fs で秘匿トークンを読まれたり、process.env からトークンを抜き取られて外部にアップロードされるのが致命的なので、やはり deno にある一式がほしい。
deno でも dynamic import で読み込む時に権限をサブセット化したい。
ネットワーク越しのスクリプトを実行するには権限の制御が必須だと思う。
細かい制御ができるようにはなっているが、 プロダクション実行以外は deno run -A
するサンプルコードが増えてる気がするので、やや怖い
そもそもの守るべき対象は何かを考え直す
- 開発用CLIの実行時に秘匿トークンを外部送信されないようにしたい
- 開発用CLIの実行時にファイルを書き換えられて任意コード実行されないようにしたい
- 本番実行スクリプトにXSSが存在した時に、環境変数を触られないようにしたい
この辺が達成できればよさそう
ドキュメント読んでて気付いたが、 Deno 1.36以降 に --deny-*
が増えている
やたら厳しくして deno run -A
で破壊されるぐらいなら、大事なリソースだけ守れればいい、みたいな発想の転換だろうか。
include と exclude で両方指定したらどっちが優先されるんだ問題が発生しそう
思い出したので node --experimental-shadow-realm も試してみた。
const realm = new ShadowRealm();
const f = realm.evaluate(`() => { return 1; }`);
console.log(f()); //=> 1
親スクリプトから独立した名前空間で評価できる
中で console.log してみる。
const realm = new ShadowRealm();
realm.evaluate(`console.log(1)`);
$ node --experimental-shadow-realm realm.mjs
file:///Users/kotaro.chikuba/sandbox/play-qwik-deno/realm.mjs:6
realm.evaluate(`console.log(1)`)
^
TypeError: ShadowRealm evaluate threw (TypeError: Cannot read properties of undefined (reading 'isTTY'))
期待通り失敗した。shadowRealm 空間では process オブジェクトが消えてそう。