Open7

deno と node の --experimental-permission を書き比べる

mizchimizchi

Node と Deno で両方で実行できるスクリプトを書く。
どちらも read 権限は与えるが write は渡さない。

scratch.js
// 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('...')

将来的に権限を考慮した両対応ライブラリを書くなら、権限周りを抽象化したラッパーライブラリを用意する必要がありそう。

mizchimizchi

比較表

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-nethttps://nodejs.org/api/permissions.html#policies で似たようなことができるので、それでオミットされてる可能性がある
mizchimizchi

私見

権限の抽象化で似たようなラッパーライブラリを用意することは可能だが、プロセスモデルが違うので細かい差異を踏むのが怖い。

サプライチェーンアタックで fs で秘匿トークンを読まれたり、process.env からトークンを抜き取られて外部にアップロードされるのが致命的なので、やはり deno にある一式がほしい。

deno でも dynamic import で読み込む時に権限をサブセット化したい。

ネットワーク越しのスクリプトを実行するには権限の制御が必須だと思う。

細かい制御ができるようにはなっているが、 プロダクション実行以外は deno run -A するサンプルコードが増えてる気がするので、やや怖い

mizchimizchi

そもそもの守るべき対象は何かを考え直す

  • 開発用CLIの実行時に秘匿トークンを外部送信されないようにしたい
  • 開発用CLIの実行時にファイルを書き換えられて任意コード実行されないようにしたい
  • 本番実行スクリプトにXSSが存在した時に、環境変数を触られないようにしたい

この辺が達成できればよさそう

mizchimizchi

ドキュメント読んでて気付いたが、 Deno 1.36以降 に --deny-* が増えている

https://docs.deno.com/runtime/manual/basics/permissions

やたら厳しくして deno run -Aで破壊されるぐらいなら、大事なリソースだけ守れればいい、みたいな発想の転換だろうか。

include と exclude で両方指定したらどっちが優先されるんだ問題が発生しそう

mizchimizchi

思い出したので 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 オブジェクトが消えてそう。