Node.js にプロセスレベルの Permission が入りそうな話

Node.js の Permission についての解説を行います。

Node.js に Permission 機能が入りそう。

すでに PR が出されており、 land も間近です。おそらく次かその次くらいのリリースで入ることになるでしょう。

github.com

おそらく初期リリースでは experimental flag を付けた上で、 fs, child_process, worker のパーミッションを許可するかどうかに留まり、 net, env などのパーミッションは今後になるでしょう。 以下の方法で利用します。

// filesystemの読み書きを許可する
$ node --experimental-permission --allow-fs foo.mjs
  • --allow-fs ファイルシステムの読み書きを行えるようにする
  • --allow-fs-read= に記載のファイルパスの読み込みを行えるようにする * を書くと全パス許可
  • --allow-fs-write= に記載のファイルパスの書き込みを行えるようにする * を書くと全パス許可
  • --allow-child-process 子プロセスの起動を行えるようにする
  • --allow-worker ワーカースレッドの起動を行えるようにする

この時点ではプロセス単位でのパーミッションとなり、フラグを付与したらプロセスレベルで有効・無効が検知されます。 プロセス単位でのパーミッションは Deno なども戦略として取っているものです。

ただし、この機能自体は実際には 4 年以上前、 Deno が登場する前から議論として存在していました。

github.com

このときは関心がまだ少なく、実装するにも議論の土台となる話ができる状態になってなかったです。

Denoなどの競合や後述するサプライチェーンアタックの状況に対して、満を持して4年の歳月を経た後に Node.js に入れることになった経緯やパーミッションについての考え方を解説します。

Node.js において Permission の機能が必要になった背景

Node.js はかなり色んなツールで使われるようになりました。サーバサイド、クライアントのビルド、デスクトップアプリ、今やフロントエンド領域では使ってない所の方が珍しいと思います。一方で、例えば適当に作ったはずのアプリケーションのライブラリの奥の奥にあるライブラリが乗っ取られて悪意のあるものに書き換えられていたらどうなるでしょう。例えば npm に名刺を登録し、 npx @yosuke-furukawa/card とやると名刺が実行されるものがあります。

npx @yosuke-furukawa/card
npx @yosuke-furukawa/card

これを何の気なしに実行したら名刺としての公開情報が出るだけですが、この中のライブラリに .ssh の中身を読み込むものがあって秘密鍵を送られるなどの被害があるかもしれません。

この手の問題は開発者のワークフロー内に攻撃を仕掛けるため、開発からリリースまでの一連のサプライチェーンに対して攻撃をするという意味でサプライチェーンアタック」と呼ばれており、近年盛り上がっている問題の一つです。

こういう問題は問題が顕著になると対策が取られます。ブラウザも Spectre, XS-Leaks などの問題が顕著になった事で様々な対策が取られてきました。 Site Isolation や Cross Origin Resource 制限系の対策は最たる例でしょう。サプライチェーンの問題は Node.js では大きく問題として受け取られてきました。そこで Permission の機能が検討されています。

Node.js の Permission を入れる時の方向性

Node.js はエコシステムが既にあります。このエコシステムを壊すことはできないため、既に存在する機能に対してオプトインする(後から付け加える)形で導入する必要があります。(この点、 Deno などの後発は Secure By Default としてデフォルトからセキュア側に倒す事もできる設計になっていますし、Denoは実際にそうなっています。)

また、プロセス単位の粒度で制限できることになります。

ちなみにプロセス単位の制御で良いのかという議論もあります。例えばモジュールレベルだったり任意の関数レベルのスコープで粒度を決める判断もあります。 しかしながら、これをやろうとすると既存のモジュールの作りに大幅に影響が出るので、最初ある「エコシステムを壊さない」に抵触するため、実際にはプロセスレベルの粒度ですすめることになりそうです。

つまり方向性としては、

  • プロセスレベルでの制御に留める
  • エコシステムを壊さない

という方向で進めようとしています。筆者は初期実装でやるレベルとしては妥当かなと思っています。

API レベルでの制御

また、この他にもAPIレベルで制御することができます。

process.permission.deny('fs'); // fs 処理を全部拒否
process.permission.deny('fs.write'); // write のみ拒否
process.permission.deny('fs.read'); // read のみ拒否
// 特定のフォルダだけ拒否
process.permission.deny('fs.write', ['/foo/bar/protected-folder']);
process.permission.deny('fs.read', ['/foo/bar/protected-folder']);

また has で拒否したかどうかを確認できます。

process.permission.has('fs'); // true or false

注意事項

Permission は現時点では以下の注意があります。

  • ネイティブモジュールは制限を受けない
  • CLI では絶対パスだけ採用される、 API では相対パスも許可
  • 子プロセスにはパーミッションが引き継がれない
  • 子ワーカーにはパーミッションが引き継がれない
  • 既に開いてしまっているリソースには制限が適用されない。できればリソースを利用する前に定義する必要がある。

最後のやつだけわかりにくいので解説します。

import fs from "node:fs";

const fd = fs.openSync("/foo/bar", "r");
// open した後に拒否する
process.permission.deny('fs');
// この場合はエラーにならず、普通に読み取れる
console.log(fs.readFileSync(fd).toString());

ルールが適用されるのは開く前の段階なので、使うなら open の前に書く必要があります。

// ここで使えばエラーになる。できれば、ステートメントが実行される前に事前に定義するほうが望ましい。
process.permission.deny('fs');
const fd = fs.openSync("/foo/bar", "r");

筆者のスタンス

experimental なので使ってみることはあれど、中々使うのが難しい機能だなと感じます。一方コミュニティは Permission の機能に関して「あるべきである」とする vote が多かったので、コミュニティの声を反映して追加されています。 実際にコミュニティが全体的に使うのかどうなのかはわかりません。

Permission が存在するべきかどうかの vote
Permission が存在するべきかどうかの vote

先行実装している Deno 等で制御しても不便になり、結局全てを allow している状況を見たりすると、「権限を適切に管理するという事自体が人類には早すぎたのかもしれない」、そんな思いを抱いています。

ちなみに Deno で開発している人の意見として、「ローカル開発では "--allow-all" だけど、本番ではちゃんと権限設定してる」という意見も見ました。

これに関しては、本番環境の場合、 OS のプロセス実行ユーザー権限でファイルアクセスを縛ることもできるし、インフラ環境次第ではリクエストフィルターによって outbound のリクエスト先を制限することもできるので、本番環境の場合では、この機能はそこまで効果的なのか不明です。多層的にシステム全体とOSとプロセスの全てで防御したいという事であれば理解します。

ただ、実際には「ローカル環境でもサプライチェーンアタックの危険性は防げないといけない」と思っています。

一方で、選択肢が増えること自体は良いことのように思えています。これから開発する時に多層的な防御機構を使って開発できるというやり方が新しく加わることになります。 コミュニティ側で使ってみて、積極的にフィードバックできるところからしていければと思っています。

もしかしたら、人類には早かったかもしれませんが、 AI とインテグレートされた近未来の開発では勝手に AI が npm 実行時に必要な権限を...みたいな妄想をしつつ、一旦ここまでとします。