権限(Capabilities)とセキュリティ
このページで分かること
- マニフェストでの権限(Capability)の宣言と、権限ごとに使えるAPIの一覧(
content:read、network:requestなど) - ネットワークの許可ホスト(
allowedHosts)と、サンドボックスが強制すること(権限による制限、ストレージとKVの分離、ネットワークの分離、ホストのバインディングを見せないこと、リソース制限) - サンドボックスでも制限できないこと、インストール・更新時の同意、バンドル時の検証
このページの目次
サンドボックス型プラグインは、デフォルトで分離されています。自分のKVとストレージの読み書き以外のことをするには、プラグインはマニフェストで権限(Capability)を宣言する必要があります。サンドボックスのブリッジは、ホストが提供するすべてのAPIを、この宣言に基づいて制限します。content:read を宣言していないプラグインには ctx.content が渡されず、network:request を宣言していないプラグインには ctx.http が渡されません。
このページでは、それぞれの権限で使えるようになるもの、サンドボックスが権限をどのように強制するか、強制できないものは何かを説明します。
権限の宣言
権限は、slug などの信頼に関する取り決め(trust contract)の項目と並べて、emdash-plugin.jsonc に書きます。
{
"slug": "plugin-hello",
// ...identity + profile...
"capabilities": ["content:read", "network:request"],
"allowedHosts": ["api.example.com"]
}
プラグインが実際に必要とするものだけを宣言します。レジストリはインストールの前にこれらの権限をサイトの運営者に表示するため、余分な宣言があるたびに、プラグインが使わないアクセスの承認を運営者に求めることになります。
やさしい解説
公式の対応表では、WordPressのプラグインは、EmDashのサンドボックス型またはネイティブ型のプラグインにあたります。サンドボックス型プラグインでは、マニフェストで宣言した権限の分だけAPIが使えるようになります。たとえば content:read を宣言していなければ、コンテンツを読むための ctx.content そのものが渡されません。宣言した権限はインストールの前に運営者に表示されるため、必要なものだけを書きます。
権限のリファレンス
| 権限 | 使えるようになるもの |
|---|---|
content:read |
ctx.content.get()、ctx.content.list() |
content:write |
ctx.content.create()、ctx.content.update()、ctx.content.delete()(content:read を含む) |
taxonomies:read |
ctx.taxonomies.getAll()、ctx.taxonomies.getTerms()、ctx.taxonomies.getEntryTerms() |
media:read |
ctx.media.get()、ctx.media.list() |
media:write |
ctx.media.getUploadUrl()、ctx.media.upload()、ctx.media.delete()(media:read を含む) |
network:request |
ctx.http.fetch()(allowedHosts に限定) |
network:request:unrestricted |
ホストの制限がない ctx.http.fetch()(ユーザーが設定するURL専用) |
users:read |
ctx.users.get()、ctx.users.getByEmail()、ctx.users.list() |
email:send |
ctx.email.send()(メールプロバイダーのプラグインが設定されている必要がある) |
hooks.email-transport:register |
排他的な email:deliver フックの登録を許可する(送信手段を提供するプラグイン向け) |
hooks.email-events:register |
email:beforeSend/email:afterSend フックの登録を許可する |
hooks.page-fragments:register |
page:fragments フックの登録を許可する(ネイティブ型プラグインのみ) |
プラグインに必要な権限は、次の規則によって変わります。
- 包含関係。
content:writeは自動的にcontent:readを含みます。media:writeはmedia:readを、network:request:unrestrictedはnetwork:requestを含みます。両方を列挙する必要はありません。 - タクソノミーは、読み取り専用の別の窓口です。
taxonomies:readを宣言すると、ctx.taxonomiesを通じて、タクソノミーの定義、そのターム、エントリーに割り当てられたタームにアクセスできます。これはcontent:readとは独立しています。プラグインがコンテンツとその分類の両方を読む場合は、両方を宣言します。プラグインからタクソノミーに書き込む手段はありません。 network:request:unrestrictedは、ユーザーが設定するURLのためにあります。 運営者が送信先のURLを入力するWebhookプラグインは、マニフェストに書かれていないホストにアクセスする必要があります。常に決まったAPIを呼び出すプラグインは、network:requestとallowedHostsを使ってください。email:sendは、権限だけでなく設定によっても制限されます。 プラグインはemail:sendを宣言できますが、ほかのプラグインがemail:deliverの送信手段を登録している場合にのみctx.emailが渡されます。
ロケールを指定したコンテンツの作成
ctx.content.create() は、新しいエントリーのロケールを指定する3番目の引数を省略可能な形で受け付けます。
const post = await ctx.content.create(
"posts",
{ title: "繁體中文" },
{ locale: "zh-tw" },
);
ロケールの照合は大文字と小文字を区別せず、サイトのロケール設定での大文字・小文字の表記で保存されます。そのため、設定されている表記が zh-TW であれば、zh-tw は zh-TW になります。明示的に指定したロケールの形式が正しくない場合は、常に例外が発生します。i18nが設定されている場合は、設定済みのロケールの一覧にないロケールを明示的に指定したときにも例外が発生します。このオプションを省略すると、EmDashはサイトで設定されたデフォルトのロケールを使います。i18nの設定がないサイトでは、デフォルトの en のままです。
ネットワークの許可ホスト
network:request を持つプラグインは、allowedHosts に列挙したホストにだけリクエストを送れます。先頭の *. は、指定したドメインとそのサブドメインの両方に一致します。
"capabilities": ["network:request"],
"allowedHosts": [
"api.example.com", // exact host
"*.cdn.example.com" // cdn.example.com and any subdomain
]
ブリッジは、リクエストを転送する前に、リクエストURLのホストを許可リストと照合します。宣言されていないホストへのリクエストは、サンドボックスの外に出ることなく、プラグインの中で例外になります。
network:request:unrestricted は、マニフェストのホストの許可リストを使いません。それでもサンドボックスのブリッジは、HTTPとHTTPSだけを受け付け、既知の内部ホストとプライベートなアドレスの直接指定をブロックし、リダイレクトのたびに再確認し、リダイレクトが別のオリジンにまたがる場合は認証情報のヘッダーを取り除きます。制限なしのアクセスは、運営者が実行時に送信先を指定する場合にだけ使ってください。送信先が決まっている場合は、同意ダイアログにホスト名が表示されるように、network:request とホストを明示して宣言してください。
サンドボックスが強制すること
サンドボックスランナーが有効な場合、ランタイムは次のことを強制します。
-
権限による制限。 PluginContextのファクトリーは、対応する権限が宣言されている場合にだけ、
ctx.content、ctx.taxonomies、ctx.media、ctx.http、ctx.users、ctx.emailを用意します。宣言していない権限のメソッドは呼び出せません。そこにはオブジェクトが存在しないためです。 -
ストレージとKVの範囲の限定。 ストレージとKVの操作はすべて、ランタイムのプラグインIDの範囲に限定されます。プラグインは、ほかのプラグインのKVやストレージのコレクションを読めず、自分のマニフェストで宣言したコレクションにだけアクセスできます。
-
ネットワークの分離。 直接の
fetch()やその他のネットワークの基本機能は、ランナーによってブロックされます。ネットワークにアクセスする唯一の方法はctx.http.fetch()で、これはブリッジによるホストの検証を通ります。 -
ホストのバインディングを渡さない。 サンドボックス型プラグインからは、環境変数、ファイルシステム、プラットフォームのバインディングが見えません。ホストのWorkerがそれらを持っていても同じです。プラグインのランタイムは、ブリッジと宣言した権限だけを持つ、まっさらなisolateです。
-
リソース制限。 Cloudflareのランナーのデフォルトは、1回の呼び出しあたりCPU時間50ミリ秒、サブリクエスト10件、経過時間30秒です。CPUとサブリクエストはWorker Loaderが強制し、経過時間はランナーが強制します。Worker Loaderにはプラットフォームのメモリー上限がありますが、プラグインごとの
memoryMbオプションは現在強制できません。Node.jsのworkerdランナーが強制するのは、デフォルトの30秒の経過時間だけです。単体のworkerdが強制できないCPU、メモリー、サブリクエストの制限をサイトで設定すると、警告を表示します。フックごとのtimeoutは、サンドボックス形式のプラグインを同じプロセス内で実行する場合にだけ適用されます。
やさしい解説
ここでの「強制」は、プラグインのコードが何をしようとしても、仕組みの側で止めるという意味です。宣言していない権限のAPIはそもそも渡されず、ほかのプラグインのデータは読めず、ネットワークには許可したホストにしか出られず、環境変数やファイルも見えません。ただし、許可した権限の範囲内で何をするかまでは制限されないため、「サンドボックスが強制しないこと」もあわせて確認します。
サンドボックスが強制しないこと
権限の仕組みでは扱わない、また扱えないことがいくつかあります。
- 許可した権限の範囲内での動作。
content:writeを持つプラグインは、自分が作ったコンテンツだけでなく、あらゆるコンテンツを編集できます。権限は大まかな単位です。「このプラグインはコンテンツに書き込める」ことを表し、「このプラグインは自分が作ったコンテンツにだけ書き込める」ことは表しません。運営者は、そのアクセスを許可する前に、プラグインのコードと公開者を評価する必要があります。 - エントリーの編集ロック。
ctx.content.update()とctx.content.delete()は、プログラムによる書き込みです。編集者がエントリーの勧告的な編集ロックを持っていても、これらはブロックされません。プラグインと編集者の両方が同じエントリーを更新する可能性がある場合は、プラグインの書き込みを編集者と調整してください。 - Node.jsでの運営者の信頼。 設定したサンドボックスランナーが利用できないと報告した場合(Cloudflare Worker Loaderがない、Node側のランナーがインストールされていない、など)、
sandboxed: []のプラグインは起動時にスキップされます。それらをplugins: []に移して同じプロセス内で実行することはできます。ただしその場合は、V8のisolateもリソース制限もなく、プラグインはfetch()を直接呼び出したり、環境変数を読んだりできます。ネイティブ型と同じ水準の信頼として扱ってください。 - サイドチャネル。 処理時間、ログ出力、保存されたデータはすべて、ホスト環境に適切なアクセス権を持つ人なら誰でも見られます。サンドボックスを、それを実行している運営者に対する機密保持の境界として使わないでください。
権限への同意
運営者がレジストリからサンドボックス型プラグインをインストールするとき、EmDashは宣言された権限を一覧にした同意ダイアログを表示します。権限を追加する更新(たとえば、これまでコンテンツを読むだけだったプラグインが、ネットワークリクエストを送ろうとする場合)は、権限の差分として表示され、新しいバージョンが有効になる前にあらためて承認が必要です。
将来使うかもしれない権限を宣言すると、インストールや更新のたびに不要なアクセスの承認を求めることになります。現在のバージョンが使う権限を列挙し、権限を使い始めるバージョンでその権限を追加します。
バンドル時の検証
emdash-plugin bundle と emdash-plugin publish は、追加で次のことを確認します。
- 宣言した権限はすべて、認識されている権限の一覧に含まれている必要があります(書き間違いがあるとビルドが失敗します)。
network:requestには空でないallowedHostsが必要です。network:request:unrestrictedではallowedHostsを空にする必要があります。権限とホストを参照してください。- バンドルした
backend.jsは、Node.jsの組み込みモジュール(fs、path、child_processなど)をインポートできません。サンドボックスのランタイムはこれらを提供しないためです。
作成時に書く項目はマニフェストのリファレンスを、バンドル時の確認はバンドルと公開を参照してください。