沙箱外掛程式預設是隔離的。要執行超出讀寫自身 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: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,但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,以便同意對話框準確告知營運者外掛程式將呼叫哪裡。
沙箱強制執行什麼
當沙箱執行器處於活動狀態時,執行時強制執行:
-
能力閘控。 PluginContext 工廠僅在宣告了相應能力時才填充
ctx.content、ctx.taxonomies、ctx.media、ctx.http、ctx.users、ctx.email。呼叫未宣告能力上的方法是不可能的 —— 那裡沒有物件。 -
儲存和 KV 範圍。 每個儲存和 KV 操作都限定在外掛程式的 slug 範圍內。外掛程式無法讀取另一個外掛程式的 KV 或儲存集合,只能存取在清單中宣告的儲存集合。
-
網路隔離。 直接的
fetch()和其他網路原語被執行器阻止。通往網路的唯一路徑是ctx.http.fetch(),它經過橋的主機驗證。 -
無宿主繫結。 沙箱外掛程式看不到環境變數、檔案系統或平台繫結 —— 即使你的宿主 Worker 擁有它們。外掛程式執行時是一個乾淨的隔離體,只有橋和宣告的能力。
-
資源限制。 執行器可以對每次呼叫強制執行 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 bundle 和 emdash-plugin publish 執行額外檢查:
- 每個宣告的能力必須在已識別的集合中(拼寫錯誤會導致建置失敗)。
network:request需要非空的allowedHosts;network:request:unrestricted需要它為空。參見清單參考。- 打包的
backend.js不能匯入 Node.js 內建模組(fs、path、child_process等)—— 沙箱執行時不提供它們。
完整檢查清單請參見打包和發布。