12k
All articles

Deno のパーミッションモデル徹底解説

Denoの権限モデルを解説。allowとdenyのフラグ、範囲指定、deno.jsonの権限セット、Permissions API、セキュリティ上の注意点まで整理。

OpenReplay Team
OpenReplay Team
Deno のパーミッションモデル徹底解説

Deno はコードをサンドボックス内で実行し、最初は何も許可しません。ファイルシステム、ネットワーク、環境変数、サブプロセス、システム情報、ネイティブライブラリ(FFI)はすべて閉じられており、--allow-* フラグで個別に有効化するまで利用できません。しかも、ほぼすべてのフラグは引数を取り、指定したパス・ホスト・変数だけに許可範囲を絞り込めます。

Node から移ってきた人であれば、最初に書いた Deno スクリプトはほぼ確実にパーミッションエラーで止まるでしょう。そして、その解決策は手癖で打ちたくなる万能の -A であることはまずありません。どのフラグを追加すべきか、どこまで厳しくスコープを切るべきかを見極めることが、学習コストの大半を占めます。

これは従来の Node.js のデフォルトとは正反対の設計であり、Deno スクリプトを実行する前に理解しておくべき最も重要なポイントです。本記事では、デフォルトで何がブロックされるのか、すべての --allow-* / --deny-* フラグとスコープ指定の構文、Deno 2.x で追加された要素(deny の優先、--allow-sys--allow-import、env のワイルドカード)、deno.json のパーミッションセット、ランタイムの Permissions API、そして公式ドキュメントが最も口を閉ざしている 2 つのセキュリティ上の落とし穴(footgun)を解説します。

要点

  • デフォルトでは、Deno のコードはファイルの読み書き、ネットワーク接続、環境変数の読み取り、サブプロセスの生成、システム情報へのアクセス、ネイティブライブラリのロードのいずれも行えません。機能ごとに --allow-* フラグでオプトインします。
  • すべての --allow-* フラグには対応する --deny-* フラグがあり、deny が常に優先されます。--allow-read=. --deny-read=./secrets はプロジェクトディレクトリを許可しつつ、./secrets は読み取り不可のままにします。
  • Deno 2 では、拒否された機能は Deno.errors.NotCapable を発生させます(旧 PermissionDenied から改名)。これにより、Deno 独自のパーミッション拒否と通常の OS エラーを区別できます。
  • Deno 2.5 以降は、deno.json に名前付きのパーミッションセットを定義し、-P=name(あるいは default セットなら -P 単体)で適用できます。最小権限のフラグをバージョン管理下に置けます。
  • 初期の静的インポートグラフに含まれるものは、ロード前にパーミッションシステムによるチェックを一切受けません。また --allow-run はサンドボックスの外でサブプロセスを実行します。これらが、信頼できないコードがサンドボックスから抜け出す 2 つの経路です。

なぜ Deno はデフォルトで安全なのか

環境由来(ambient)の権限で動くものは何もありません。ディスク、ネットワーク、環境、サブプロセス生成は、あなたが開くまですべて閉じられています。この設計判断は、Node の生みの親である Ryan Dahl 自身によるものです。彼は Node の「すべてにフルアクセス」というデフォルトを反転させるために Deno を作りました。Deno では依存関係がそれ自体の ambient authority を持ちません。一方 Node では、パッケージは周囲のプロセスが到達できるあらゆるシステム I/O をそのまま継承します。この差が、両ランタイムの最も鮮明な違いです。

Node もその後、独自のパーミッションモデルを追加しました。Node 20 で --experimental-permission として実験的に導入され、v23.5.0 で安定版となり、Node 24 で実験的な綴りが廃止されて単なる --permission になりました。それでも Deno のモデルはより踏み込んでいます。オプトインのフラグではなくデフォルトであり、より多くの機能クラスをより細かいスコープでカバーします。

Deno の —allow-* パーミッションフラグとは

各機能は 1 つのフラグに対応し、ほとんどのフラグは許可リスト引数を受け取ります。フラグ単体ならそのクラス全体を許可し、引数を付ければ範囲が絞られます。--allow-net 単体はすべてのホストのすべてのポートへのアクセスを許可しますが、--allow-net=api.example.com:443 はプログラムを 1 つのホストとポートだけに限定します。

フラグ保護対象スコープ指定の例対応する deny
--allow-readファイルシステムの読み取り--allow-read=./data,config.ini--deny-read
--allow-writeファイルシステムの書き込み--allow-write=./tmp--deny-write
--allow-netネットワークアクセス--allow-net=api.example.com:443--deny-net
--allow-env環境変数--allow-env=PORT,HOST--deny-env
--allow-runサブプロセス--allow-run=git,deno--deny-run
--allow-sysシステム情報 API--allow-sys=hostname--deny-sys
--allow-ffiネイティブライブラリ--allow-ffi=./lib.so--deny-ffi
--allow-importリモートの HTTPS インポート--allow-import=jsr.io--deny-import

なお --allow-hrtime は存在しません。このフラグは Deno 2.0 で削除され、performance.now() のような高解像度タイミング API は現在では常に利用可能です。

スクリプトが付与されていないパーミッションを必要とすると、Deno は処理を一時停止して対話的に確認します。

┏ ⚠️ Deno requests net access to "deno.com:443".
┠─ Requested by `fetch()` API.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

y で一度だけ許可、n で拒否(Deno.errors.NotCapable が発生します)、A でそのクラス全体を許可します。CI ではプロンプトで処理が止まらないよう、フラグを事前に渡してください。

フラグ群の拡張の歴史: deny の優先、--allow-sys--allow-import、env のワイルドカード

deny フラグは Deno 1.36(2023 年 8 月)で導入され、以降すべての --allow-* フラグに対応する --deny-* が用意されています。両者が重なる場合は拒否が適用されるため、広く許可しつつ例外を切り出すことができます。

deno run --allow-read=. --deny-read=./secrets app.ts

--allow-sys は Deno 1.26(2022 年 10 月)に遡るもので、Deno.hostname()Deno.systemMemoryInfo() といったシステム情報 API を制御します。Deno 2.0 で真に新設された機能クラスは --allow-import で、実行時にコードがどの HTTPS ホストからモジュールを取得できるかを制御します。素の HTTP は決して許可されず、静的インポートは自動的にこのリストと照合され、自分でホストを指定すると Deno の組み込みセットに追加されるのではなく置き換えられます。特定のホストを完全にブロックするには --deny-import を使います。

環境変数へのアクセスは、Deno 2.1 でサフィックスのワイルドカードに対応しました。変数を 1 つずつ列挙する代わりに、プレフィックスでスコープを指定できます。

deno run --allow-env="AWS_*" main.ts

deno.json でのパーミッション宣言

Deno 2.5 以降は、deno.json に名前付きのパーミッションセットを定義し、-P=name(または --permission-set=name)で適用できます。実行ごとにフラグを打ち直す代わりに、最小権限の設定をバージョン管理下に置けます。オブジェクトのキーはフラグ名(readwritenetenvsysrunffiimport)で、deno.json リファレンスに記載されています。

{
  "permissions": {
    "default": {
      "read": ["./deno.json"],
      "env": true,
      "run": { "allow": ["git"] }
    },
    "process-data": {
      "read": ["./data"],
      "write": ["./data"]
    }
  },
  "tasks": {
    "dev": "deno run -P main.ts"
  }
}

名前付きセットを使うには deno run -P=process-data main.tsdefault セットを使うには deno run -P main.ts を実行します。Deno 2.5 では DENO_AUDIT_PERMISSIONS 環境変数も追加されました。ファイルパスを指定すると、プログラムが触れたすべてのパーミッションについて、許可されたか拒否されたかを問わず JSONL のエントリが追記されます。スクリプトが本当に必要としているものを手早く把握する方法として便利です。

ランタイムの Permissions API

制限された操作の前にコード内でパーミッションを照会すれば、NotCapable エラーでクラッシュせずに穏当に失敗させられます。Deno.permissionsqueryrequestrevoke を公開し、それぞれ { name: "net", host: "example.com" } のようなディスクリプタを受け取ります。

const desc = { name: "net", host: "example.com" } as const;

let status = await Deno.permissions.query(desc); // "prompt" | "granted" | "denied"
if (status.state === "prompt") {
  status = await Deno.permissions.request(desc); // triggers the y/n/A prompt
}

if (status.state === "granted") {
  await fetch("https://example.com");
}

await Deno.permissions.revoke(desc); // drop it again

query はプロンプトを出さずに現在の状態を返し、request は状態がまだ prompt の場合にユーザーへ確認し、revoke は付与された機能を取り消します。これにより、長時間動作するプログラムはリソースに触れる前に確認し、アクセスできない場合には別の経路を取れます。

2 つの落とし穴: インポートと --allow-run

信頼できないコードがサンドボックスを回避できる挙動が 2 つあり、どちらも頭に入れておく価値があります。

1 つ目は、エントリポイントから Deno が静的に解決できるすべてのもの(ローカルファイル、npm や JSR のパッケージ、文字列リテラルとして書かれたリモート URL)が、パーミッションシステムの判断を経る前に取得されるという点です。つまり依存関係は、最初の --allow-* フラグが適用される前に、自身のソースを読み、ネットワークに到達できます。ただしこの「フリーパス」はロードに限られます。コードが実行された瞬間から、あらゆる操作は再びチェックされます。--allow-importどのリモートホストからインポートできるかを制限しますが、インポート自体に実行時の許可を要求するようにはしません。したがって、サードパーティのコードは取り込む前に監査してください。

2 つ目、--allow-run は最も鋭い落とし穴です。生成したものは独立したプロセスとなり、Deno に渡した限定的な権限ではなく、OS が与える権限を引き継ぎます。つまり --allow-run=deno は、サンドボックス化されたスクリプトが --allow-all で Deno を再起動し、完全に脱出することを許してしまいます。さらに、制限されるのはどの実行ファイルを走らせるかだけで、その引数は制限されません。--allow-run=cat を与えれば、コードは cat 経由で任意のファイルを読めます。--allow-run=git のように、信頼できる特定のバイナリに絞ってください。また --allow-ffi も同じクラスのリスクを伴うことに注意してください。ネイティブライブラリは JavaScript レイヤーのチェックの外側でマシンコードとして実行されます。

実践的なスタンスはこうです。動作する最小限の許可リストを与え、機微なパスには --deny-* を重ね、--allow-run--allow-ffi は「便利機能」ではなく「信頼境界」として扱うこと。パーミッションゼロから始め、スクリプトを実行し、プロンプト(または DENO_AUDIT_PERMISSIONS のログ)が必要だと示したものだけを厳密に戻していきます。

FAQ

Deno のパーミッションプロンプトで「いいえ」と答えることと Deno.errors.NotCapable の違いは何ですか?

どちらも入口が違うだけで結果は同じです。対話的なプロンプトで 'n' と答えた場合、あるいは必要なフラグなしで実行した場合、拒否された操作は Deno 2 では Deno.errors.NotCapable を発生させます(旧 PermissionDenied から改名)。この改名により、ファイルが見つからないといった通常の OS エラーと、Deno 独自のパーミッション拒否を区別できます。以前はどちらも似た見た目の失敗として現れていました。

--allow-net=example.com はポート 443 の HTTPS も許可しますか?

はい。--allow-net=example.com のようにポートを指定せずにホストを指定した場合、Deno は 443 を含む任意のポートへの接続をそのホストに対して許可します。単一のポートに限定するには --allow-net=example.com:443 と明示的に書く必要があり、その場合そのホストの他のすべてのポートはブロックされます。引数なしの --allow-net はすべてのホストのすべてのポートを許可します。

--allow-read と --deny-read を重なり合うパスに対して併用できますか?

はい、そして deny が常に優先されます。--allow-read=. --deny-read=./secrets を実行すると、./secrets を除くプロジェクトディレクトリ全体への読み取りアクセスが付与され、./secrets は読み取り不可のままになります。deny フラグはすべての機能クラスで allow フラグに優先するため、このパターンにより、許可するファイルを 1 つずつ列挙するのではなく、広く許可しつつ機微なパスを切り出すことができます。

npm や JSR のパッケージを使うには --allow-import が必要ですか?

いいえ、静的にインポートされるパッケージには不要です。npm や JSR のパッケージを含め、コードを実行せずにエントリポイントから Deno が解決できるものは、パーミッションシステムが参照される前に取得されます。--allow-import はリモートインポートの取得元となる HTTPS ホストを決めるもので、素の HTTP は選択肢になりません。実行時に計算される指定子は別です。動的なリモート URL には --allow-import が必要で、動的なローカルパスには --allow-read が必要です。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.