Deno の Permission の話

Deno (ディノ) Advent Calendar 2020、1日目の記事です。

今日は Deno の Permission の話をします。

# Op と Permission の関係

Deno の JavaScript 実行環境は sandbox 化されていて、直接外部の環境を参照したり・操作をすることは出来ないようになっています。ここでいう「外部の環境」というのは、ファイルシステム、ネットワーク、環境変数など、純粋な JavaScript の計算以外のあらゆるコンピュータ上のリソースを指します。外部環境へアクセスするには Op という命令の単位を JavaScript 側のコードから発行して Deno 本体に解釈・実行してもらう必要があります。

各 Op には必要な権限が定義されています。例えば、op_fetch という Op があります。この Op を発行すると与えられた URL へ HTTP リクエストを実行します (つまり [Fetch API]( https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) に対応した動作をします)。この Op を発行する際には `net` パーミッションがチェックされます。適切なパーミッションを持っていなければその Op は PermissionError という例外を throw して、実行が失敗します。

# 7つのパーミッション

現在 Deno には以下の 7 種類の Permission が定義されています。

- `read` ファイル読み込みを許可
- `write` ファイル書き込みを許可
- `net` ネットワークアクセスを許可
- `env` 環境変数アクセスを許可
- `hrtime` 高精度時刻へのアクセスを許可
- `run` 外部コマンド実行を許可
- `plugin` プラグインの読み込みを許可

これらのパーミッションはコマンドライン引数で `--allow-net` のようなフラグで与えるのが普通です。

```shell
deno run --allow-net my_script.ts
```

たとえば、上の呼び出しでは `net` パーミッションを与えた状態で、my_script.ts が実行されます。

複数のパーミッションを与えたい場合は、与えたいフラグをすべて列挙します。

```shell
deno run --allow-read --allow-write --allow-net my_script.ts
```

上の例では、my_script.ts には `read`, `write`, `net` のパーミッションが与えられます。

パーミッションはスクリプト実行時に与えなくても、プログラム内から動的に要求する事もできます。次のコードは、実行時にファイルへの `read` パーミッションを要求します。

```ts
const { state } = await Deno.permissions.request({ name: 'read' })
 
if (state === 'granted') {
  console.log('I have read permission!')
}
```

このスクリプトを実行すると、以下のように prompt で権限を与えるかどうかを聞いてきます。(動的に permission をリクエストする機能はまだ unstable 機能のため、実行時に `--unstable` フラグが必要になっています。)

```shell-session
$ deno run --unstable main.js 
⚠️  Deno requests read access. Grant? [g/d (g = grant, d = deny)] 
```

g = grant を選択すると、パーミッションを与え、d = deny を選ぶとパーミッションを拒否します。

## run と plugin は注意を要するパーミッション

`run` と `plugin` は特に注意を要するパーミッションです。run を許可すると、例えば以下のようなコードでスクリプトが自分自身の権限を昇格出来るため、実質的にすべてのパーミッションを与えたのと同じことになります。

```ts
Deno.run({ cmd: ['deno', 'run', '--allow-all', import.meta.url] })
```

(すべての権限を与えて自分自身を再実行出来てしまう。)

`plugin` というパーミッションは、Deno の native plugin の利用を許可するというパーミッションです。Native plugin は Rust や C++ などで Deno の Op を自由に追加できるという機能で、Deno のセキュリティモデルと無関係にあらゆるコードが実行されうる可能性があります。plugin を利用する場合は信用できる開発元からリリースされたものであるかどうか等の点に注意が必要です。

(なお、余談ですが、Deno の Plugin システムは機能自体の削除が現在検討されています。

https://github.com/denoland/deno/issues/8490

理由の一つとして、上のような Deno のセキュリティモデルとの相性が良くないという理由が上げられています。)

# 実用的な4つのパーミッション

7 つのパーミッションのうちで、特にアプリケーション上でよく使うのは、`read`, `write`, `net`, `env` の4つのパーミッションでしょう。

この内 `read`、`write`、`net` に対しては、更にどの範囲のパス・ドメインに許可を与えるのかを細かく指定することが出来ます。

```shell
deno run --allow-read=/home/kt3k my_script.ts
```

上の呼び出しでは、my_script.ts には `/home/kt3k` 以下のファイルのみ読み込み権限が与えられます。それより上の path、たとえば `/etc/passwd` などにはアクセスすることは出来ません。

```shell
deno run --allow-net=example.com my_script.ts
```

上の例では my_script.ts には example.com ドメインへのネットワークアクセスが許可されます。それ以外のドメインへのアクセスは許可されません。複数のドメインへの許可を与えたい場合は以下のように、すべてのドメインをカンマ区切りで列挙します。

```shell
deno run --allow-net=twitter.com,facebook.com my_script.ts
```

(このような機能はパーミッションのホワイトリスト機能と呼ばれます)

# パーミッションの実践的な利用方法

最後に、具体的にプログラムを実行する際に、付与すべきパーミッションの例を紹介します。

たとえば、リンターのようなプログラムを考えてみましょう。リンターはレポジトリ内のソースコードを読み取って、コーディングルールに則った書き方をしているか確認するツールです。リンターを起動するための適切なパーミッションは次のようになるでしょう。

```shell
deno run --allow-read=. linter.ts
```

リンターはソースコードを読んで、ルールを判定するためのプログラムなので、`read` パーミッションがあれば十分です。また、読む必要があるのはレポジトリ内のファイルだけで十分なため、read ホワイトリストに `.` (カレントディレクトリ) を指定すれば十分です。

次に、フォーマッターのようなプログラムを考えてみましょう。フォーマッターは、ソースコードを読み取って、必要があれば、ソースコードを整形して上書きするようなプログラムです。フォーマッターを実行する際のパーミッションは以下のようになるでしょう。

```shell
deno run --allow-read=. --allow-write=. formatter.ts
```

フォーマッターはファイルに書き込む必要があるため、`write` パーミッションを与えています。また `write` パーミッションも、全体に与えるのではなく、`.` (カレントディレクトリ) に与えれば十分です。パーミッションの範囲をカレントに絞ることで、万が一悪意のあるソースコードがプログラム(やその依存関係のどこか)に含まれていたとしても、被害を最小限に食い止めることが出来ます。

最後に、Web サーバーの例を考えてみましょう。たとえば、2つのバックエンドサーバー (例として、ホスト名を redis.host:6379、postgres.host:5432 とします) と通信して、環境変数からクレデンシャルを読み取り、localhost の port 3000番で起動するようなサーバーを考えてみましょう。与えるパーミッションは次のようになるでしょう。

```shell
deno run --allow-env --allow-net=redis.host:6379,postgres.host:5432,localhost:3000 my_server.ts
```

まず `--allow-env` で環境変数へのアクセス権を与えています。`--allow-net` のホワイトリストで、redis.host:6379、postgres.host:5432、localhost:3000 の3つを許可しています。このサーバーが稼働するためにはこれらの条件で十分のはずです。このようにネットワーク全体アクセスではなく、ホワイトリストに絞ってアクセスを許可することで、必要のないサーバーへの通信をすべて遮断することが出来ます。

このように最低限に絞ったパーミッションのみを与えることによって、サーバーの依存関係のどこかに万が一第3者による攻撃コードが紛れ込んでしまっていたとしても、被害を最小限に抑えることが出来ます。たとえば、この例では、何らかの機密情報を盗むような攻撃を考えた場合に、攻撃者のサーバーへ到達する手段が存在しないため、攻撃が成立しません。

# まとめ

今日は Deno の Permission の仕組みと使い方を紹介しました。パーミッションを使いこなすことで、様々なリスクから自分の PC やサーバーを守ることが出来ます。Permission を使いこなしてセキュアなプログラミングをしていきましょう!