能力與安全

本頁內容

沙箱外掛程式預設是隔離的。要執行超出讀寫自身 KV 和儲存之外的任何操作,外掛程式必須在其清單宣告能力。沙箱橋根據這些宣告控制每個宿主提供的 API —— 未宣告 content:read 的外掛程式無法獲得 ctx.content,未宣告 network:request 的外掛程式無法獲得 ctx.http

本頁介紹每個能力授予什麼權限、沙箱如何強制執行它們,以及哪些是無法強制執行的。

宣告能力

能力位於 emdash-plugin.jsonc 中,與 slug 和其餘信任契約一起:

{
	"slug": "plugin-hello",
	// ...身分 + 設定...

	"capabilities": ["content:read", "network:request"],
	"allowedHosts": ["api.example.com"]
}

只宣告外掛程式實際需要的能力。能力宣告也是 Marketplace 在同意對話框中向營運者展示的內容 —— 額外的能力是安裝時的摩擦和稽核時的安全信號。

能力參考

能力授予存取權限
content:readctx.content.get()ctx.content.list()
content:writectx.content.create()ctx.content.update()ctx.content.delete()(隱含 content:read
taxonomies:readctx.taxonomies.getAll()ctx.taxonomies.getTerms()ctx.taxonomies.getEntryTerms()
media:readctx.media.get()ctx.media.list()
media:writectx.media.getUploadUrl()ctx.media.upload()ctx.media.delete()(隱含 media:read
network:requestctx.http.fetch() —— 限制為 allowedHosts
network:request:unrestrictedctx.http.fetch() 無主機限制(僅用於使用者設定的 URL)
users:readctx.users.get()ctx.users.getByEmail()ctx.users.list()
email:sendctx.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:readmedia:write 隱含 media:readnetwork:request:unrestricted 隱含 network:request。你不需要同時列出兩者。
  • 分類法是一個獨立的唯讀表面。 taxonomies:read 透過 ctx.taxonomies 授予對分類法定義、其術語以及分配給條目的術語的存取權限。它獨立於 content:read —— 如果外掛程式讀取內容及其分類,請同時宣告兩者。外掛程式沒有對分類法的寫入權限。
  • network:request:unrestricted 用於使用者設定的 URL。 營運者輸入目標 URL 的 Webhook 外掛程式需要存取不在清單中的主機。始終呼叫已知 API 的外掛程式應使用 network:request + allowedHosts
  • email:send 由設定控制,不僅僅是能力。 外掛程式可以宣告 email:send,但 ctx.email 只有在另一個外掛程式註冊了 email:deliver 傳輸時才會被填充。

網路主機允許清單

具有 network:request 的外掛程式只能擷取 allowedHosts 中列出的主機。支援子網域萬用字元:

"capabilities": ["network:request"],
"allowedHosts": [
	"api.example.com",     // 精確主機
	"*.cdn.example.com"    // cdn.example.com 的任何子網域
]

橋在轉發請求之前會將請求 URL 的主機與允許清單進行核對。對未宣告主機的請求會在外掛程式內部擲出例外,永遠不會離開沙箱。

network:request:unrestricted 完全跳過允許清單檢查。它適用於營運者在執行時設定目標 URL 的外掛程式(Webhook 發送器、通用 HTTP 轉發器)。對於目標是外掛程式設計一部分的外掛程式,請避免使用它 —— 而是使用明確的主機宣告 network:request,以便同意對話框準確告知營運者外掛程式將呼叫哪裡。

沙箱強制執行什麼

當沙箱執行器處於活動狀態時,執行時強制執行:

  1. 能力閘控。 PluginContext 工廠僅在宣告了相應能力時才填充 ctx.contentctx.taxonomiesctx.mediactx.httpctx.usersctx.email。呼叫未宣告能力上的方法是不可能的 —— 那裡沒有物件。

  2. 儲存和 KV 範圍。 每個儲存和 KV 操作都限定在外掛程式的 slug 範圍內。外掛程式無法讀取另一個外掛程式的 KV 或儲存集合,只能存取在清單中宣告的儲存集合。

  3. 網路隔離。 直接的 fetch() 和其他網路原語被執行器阻止。通往網路的唯一路徑是 ctx.http.fetch(),它經過橋的主機驗證。

  4. 無宿主繫結。 沙箱外掛程式看不到環境變數、檔案系統或平台繫結 —— 即使你的宿主 Worker 擁有它們。外掛程式執行時是一個乾淨的隔離體,只有橋和宣告的能力。

  5. 資源限制。 執行器可以對每次呼叫強制執行 CPU、子請求、掛鐘時間和記憶體限制。確切限制取決於你使用的執行器;Cloudflare 執行器使用平台的 Worker Loader 限制(每次呼叫 50ms CPU、10 個子請求、30 秒掛鐘時間、約 128MB 記憶體)。Node.js workerd 執行器(@emdash-cms/sandbox-workerd)透過 Promise.race 強制掛鐘時間;CPU 和記憶體限制是 Cloudflare 平台功能,獨立的 workerd 不強制執行。超過執行器限制的鉤子會被取消;EmDash 鉤子逾時(鉤子設定中的 timeout)額外強制執行更嚴格的上限。

沙箱不強制執行什麼

能力系統不涵蓋且無法涵蓋的一些事項:

  • 授予能力範圍內的行為。 具有 content:write 的外掛程式可以編輯任何內容,而不僅僅是自己的。能力是粗粒度的 —— 它們說「這個外掛程式可以寫入內容」,而不是「這個外掛程式只能寫入它建立的內容」。稽核時的審查是對外掛程式在其授權範圍內實際行為的唯一檢查。
  • Node.js 上的營運者信任。 如果設定的沙箱執行器報告不可用(沒有 Cloudflare Worker Loader,沒有安裝 Node 端執行器等),sandboxed: [] 外掛程式在啟動時會被跳過。你可以將它們移動到 plugins: [] 以在行程內執行 —— 但那樣就沒有 V8 隔離體、沒有資源限制,外掛程式可以直接呼叫 fetch() 或讀取環境變數。將此視為原生等級的信任。
  • 旁通道。 時序、日誌輸出和儲存的資料對任何合理存取宿主環境的人都是可見的。不要將沙箱用作針對執行它的營運者的機密性邊界。

能力同意

當營運者從 Marketplace 安裝沙箱外掛程式時,EmDash 會顯示一個包含宣告能力的同意對話框。新增能力的更新 —— 例如,一個之前只讀取內容的外掛程式現在想要發起網路請求 —— 會顯示為能力差異,並在新版本生效前需要重新批准。

這就是為什麼宣告額外的能力很重要,即使你「以後可能需要它們」。它們在每次安裝和更新時都會顯示為摩擦,安全稽核會標記要求超出明顯需要的外掛程式。準確列出外掛程式使用的內容,並在外掛程式實際開始使用時在真正的版本中新增新能力。

建置時驗證

emdash-plugin bundleemdash-plugin publish 執行額外檢查:

  • 每個宣告的能力必須在已識別的集合中(拼寫錯誤會導致建置失敗)。
  • network:request 需要非空的 allowedHostsnetwork:request:unrestricted 需要它為空。參見清單參考
  • 打包的 backend.js 不能匯入 Node.js 內建模組(fspathchild_process 等)—— 沙箱執行時不提供它們。

完整檢查清單請參見打包和發布