WebMCP 的工具存取、網站登入與業務權限,是三個分開處理的問題。瀏覽器決定文件能否使用或存取 Tool,後端識別目前使用者,再檢查這位使用者是否有權執行操作。
今天用公開商品頁、會員訂單頁與管理員編輯頁示範:工具清單隨身分切換,而每次 API 請求仍由後端驗證。Member 可以查自己的訂單;換成其他人的訂單編號,後端就回傳 HTTP 403。
我會把安全分成:
Layer 1:Browser / Origin
誰的 Document 可以存取 Tool?
Layer 2:Application Session
目前使用者是誰?
Layer 3:Business Authorization
這個使用者能不能做這件事?
工具出現在清單裡,只表示目前頁面提供這項功能。能取得哪些資料、能修改哪些內容,還要通過後端的身分與權限檢查。
圖片 1|Origin、Session 與業務權限三層邊界
WebMCP Draft 目前把 API 整合進 tools Permissions Policy,default allowlist 是:
'self'
Origin 由協定、主機與連接埠組成,例如 localhost:8080 與 localhost:8081 是不同 Origin。tools Permissions Policy 控制文件能否使用 WebMCP API;跨 Origin iframe 需要另外處理政策允許條件。
同時,Tool 註冊還能指定:
await document.modelContext.registerTool(tool, {
exposedTo: ['https://trusted.example']
});
exposedTo 指定文件樹內可存取該 Tool 的跨 Origin 對象;它與 API 的 Permissions Policy 是不同檢查,也不會授予會員或管理員權限。本日示範回應 Permissions-Policy: tools=(self),不設定 exposedTo;iframe 操作接續 Day 22。
網站使用 Cookie Session 時,Tool 透過同 Origin API 請求帶上目前的 Cookie,由後端識別使用者。WebMCP 沿用網站的登入狀態,不會另外建立一個具有額外權限的 Agent 帳號。
本例切換為 Member 後,後端 Session 對應 User 123;未登入時查詢訂單,API 回傳 HTTP 401 與 unauthenticated。身分由 Session 取得,不由 Tool 的 Arguments 指定。
登入成功後,後端還要核對資料所有權。本例訂單 1234 屬於 User 123,訂單 999 屬於另一位示範會員。
收到 GET /api/orders/999 時,後端依 Session 取得使用者,再比較訂單的擁有者:
if order['ownerId'] != session['userId']:
return self.fail(403, 'forbidden')
因此,單純更換訂單編號不會取得其他會員的資料。在 WordPress 中,管理操作同樣要搭配 current_user_can(...) 檢查能力,並依資料類型核對所有權。
例如:
get_favorite_products
get_order_history
get_saved_addresses
它們 readOnlyHint: true,但結果可能非常私人。
所以:
readOnly
≠ public
≠ safe to expose cross-origin
Chrome Security Guidance 也特別提醒,read-only Tool 仍可能透露 User Data。
工具清單依目前頁面需要的功能縮小,不把所有管理功能註冊到每個頁面。本例切換身分時,也切換對應的頁面情境:
search_products、get_product。get_orders、get_order。save_draft、prepare_publish。每次切換都先移除上一組工具,再註冊新工具。Admin 編輯頁只提供草稿與預覽功能,不累加商品或訂單工具。
python day21_server.py,保持服務運行。本日需要 Python API 後端;靜態檔案伺服器不提供 Session 與權限檢查。角色按鈕是本地教學用登入入口,商品、訂單與草稿皆為示範資料。正式網站的登入入口需接上自己的帳號驗證。
search_products、get_product。圖片 2|Guest 的公開商品工具
get_orders、get_order。圖片 3|Member 的會員訂單工具
get_order。{"orderId":1234}
httpStatus: 200、status: "success"。ownerId 為 123,與目前登入會員相符。圖片 4|查詢本人訂單,回傳 HTTP 200
get_order Tool。{"orderId":999}
httpStatus: 403、status: "forbidden"。/api/orders/999 與 403,回傳內容沒有訂單明細。圖片 5|查詢非本人訂單,回傳 HTTP 403
這兩次呼叫使用相同的 Tool,差別只有訂單編號。結果顯示:讀取工具仍會核對資料所有權,工具可見性不會取代後端授權。
save_draft、prepare_publish。圖片 6|Admin 的草稿與預覽工具
save_draft 保存本地草稿,prepare_publish 產生草稿預覽,兩者都不會發布文章。
完成後按「下載紀錄 JSON」,保存這次 API 請求紀錄,再按「登出為 Guest」。登出完成訊息會保留在頁面,工具清單恢復為商品搜尋與讀取功能。重新整理會清除本頁顯示的請求紀錄;切換身分、登出或重啟服務會清除該 Session 的草稿。
不要:
exposedTo: ['*']
目前 API 本身就要求具體安全 origins 的思路;即使未來能力改變,也不應把使用者資料 Tool 廣泛公開。
我會像設計 CORS/OAuth redirect URI 一樣:
誰真的需要?
為什麼需要?
可以只開 read-only 嗎?
資料是否含個資?
是否有稽核紀錄?
Description 用來說明工具的使用情境,例如:
Only admins should call this tool.
這段文字能提示 Agent,但實際權限由後端程式檢查。以下是本例保存草稿 API 的前端呼叫方式,input 包含 title 與 body:
async function saveDraft(input) {
const sessionResponse = await fetch('/api/session', {
credentials: 'same-origin'
});
const session = await sessionResponse.json();
const response = await fetch('/api/admin/draft', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': session.csrf ?? ''
},
body: JSON.stringify(input)
});
const result = await response.json();
return { httpStatus: response.status, ...result };
}
後端依序檢查請求 Origin、Session、CSRF token 與管理員角色,再處理草稿。未登入回傳 401;已登入但不具管理員權限回傳 403。即使從工具清單以外直接呼叫 API,也會經過同一套檢查。
[ ] Tool 是否只在需要的頁面/狀態存在?
[ ] 是否正確標 readOnly / consequential / untrusted?
[ ] 是否含私人資料?
[ ] 是否需要登入?
[ ] Server 是否重新驗證 Authorization?
[ ] 是否真的需要 exposedTo?
[ ] Cross-origin allowlist 是否最小化?
[ ] 高風險 Action 是否有 explicit confirmation?
[ ] Result 是否避免敏感內部資訊?
exposedTo 指定跨 Origin 的工具暴露對象,不會授予資料存取權限。