1. 為什麼在 ChatGPT‑App 中必須嚴肅看待權限(這裡的特殊風險是什麼)
在「一般」的 Web 應用裡,使用者與你的資料庫之間只有少數幾層:前端、API、資料庫。在 ChatGPT‑App 中,使用者與 API 之間又多了一位主動參與者——LLM。這不只是「文本過濾器」,而是一個會:
- 自行決定該呼叫哪些工具、帶哪些參數;
- 可能被資料中的prompt 注入所欺騙;
- 可能「混淆」工具或捏造你未預期的參數。
如果給了 LLM 過多權限,你就會遇到典型的 Confused Deputy 問題: 模型會善意地執行它認為使用者或文件文本要求的事,但實際卻呼叫了 delete_all_orders 而不是 get_last_order。
因此,我們的目標:
- 最小化 auth_token 的權限(可存取哪些資料與行為)。
- 限制在特定場景下模型實際可用的工具。
- 加入人在迴路的人為控管,特別是在後果嚴重之處。
同時需要避免過度恐慌與一刀切,否則 App 將變得無用。在便利與安全之間取得平衡——就是本模組的核心任務。
2. 生態系中的存取模型:誰能碰到什麼
為了不混淆,我們先看整個系統。系統有多個層級,每個層級都有各自的責任與權限。
flowchart TD U[ChatGPT 中的使用者] --> C[ChatGPT UI + LLM] C --> A["你的 App(視覺化計畫 + 小工具)"] A --> G[MCP Gateway / API Edge] G --> S[MCP 伺服器與微服務] S --> D[資料庫、佇列、外部 API]
角色速記:
- ChatGPT UI 與 LLM:由 OpenAI 管理。你可以提供指示(system‑prompt、tool descriptions),但無法控制平台的內部權杖與權限。
- 你的 App(計畫、tools、小工具):你決定哪些工具可用、如何描述、需要哪些 UX 確認、以及小工具可顯示哪些資料。
- MCP Gateway / API Edge:在這裡進行權杖檢查、對 userId、tenantId、scopes 清單做對應,並路由到正確的服務。
- MCP 伺服器與微服務:執行工具、對資料庫與外部 API 發起請求。這裡應有最嚴格的檢查:scopes、tenant 隔離、輸入驗證。
- 儲存體與外部 API:最後一道防線(資料庫層級限制、外部服務帳號的權限)。
關鍵想法:LLM 不是權限的來源。所有送到 MCP 伺服器的請求,我們都視為「由模型代為表述的使用者請求」。是否真的可執行該操作,是你的後端程式碼的責任,而不是 prompt 的責任。
3. AuthN 與 AuthZ:我們已會的與要補上的
在「身分驗證」模組中,你已經做過:
- AuthN(Authentication)——判定這位使用者是誰。透過 OAuth 2.1/PKCE,ChatGPT 從 IdP 取得權杖,隨後附加在對 MCP 的呼叫中。權杖包含 sub、user_id 或類似欄位,有時還有 tenant_id。
- 基礎的 AuthZ——你也許已能區分 user/admin,至少檢查「是否為一般使用者」或「是否為管理員」。
現在我們要更進一步:
- 每個 auth_token 都必須攜帶一組scopes——以 resource:action 形式的字串權限,例如 catalog:read、orders:write、payments:create;
- 你的 MCP 伺服器必須針對每個動作檢查這些 scopes 是否相符,而不是「只在入口檢一次」;
- 不同工具,甚至同一工具中的不同操作,都可能需要不同的 scopes。
在 OAuth 2.1 的語境中,ChatGPT 是「public client」,MCP 是「resource server」,而你的 OAuth 伺服器知道支援哪些 scopes 以及其含義。MCP 資源的中繼資料通常會宣告 scopes_supported,好讓 ChatGPT 能向使用者精準請求所需的授權。
4. 為 GiftGenius 設計 scopes
以我們的教學專案 GiftGenius 為例,先看它有哪些資料領域與動作。功能大致包括:
- 瀏覽目錄與禮物卡片;
- 基於歷史紀錄的推薦;
- 建立訂單;
- 啟動結帳/扣款;
- 管理介面編輯目錄。
與其做一個全能的 giftgenius:full_access,不如拆成合理的 scopes。
命名約定:resource:action
好用的策略是 resource:action,其中:
- resource 描述資料領域:catalog、recommendations、orders、payments、admin。
- action 描述動作類型:read、write,有時更細:create、delete、manage。
GiftGenius 的範例:
| Scope | 允許的操作 |
|---|---|
|
讀取公開的禮物目錄 |
|
讀取使用者的推薦歷史 |
|
建立新訂單 |
|
讀取使用者的訂單歷史 |
|
啟動付款/結帳 |
|
編輯目錄(僅供管理 UI/客服) |
一般 GiftGenius 使用者需要(以空格分隔): catalog:read recommendations:read orders:write orders:read payments:create。 管理員再加上 catalog:admin。
重點:不要做萬用 *:* 或 admin:all。越細的粒度,越容易在不破壞整體應用的前提下,撤回某一特定權限。
Scope 類型:read vs write vs critical
將 scopes 腦中分級很有用:
- 安全(read):不改變狀態,最多只是暴露資料;
- 會改動(write):建立/修改實體、增加計數,但不動到金流也不會肆意刪除;
- 關鍵(critical):支付、刪除帳號、大量刪除資料。
對於關鍵權限可以提高控制:
- 只授予最少數的使用者;
- 在 ChatGPT 的 UI 簽發權杖時,向使用者另外請求同意;
- 在 MCP 端要求額外驗證(例如一次性 PIN;屬於進階情境)。
在程式碼中的 scopes:RequestContext 與 requireScope
在 MCP 層級設計統一的請求脈絡型別很方便:
// mcp/context.ts
export interface RequestContext {
userId: string; // 誰
tenantId: string; // 所屬哪個組織
scopes: string[]; // 權杖擁有哪些權限
}
// 用於檢查權限的簡單 helper
export function requireScope(
ctx: RequestContext,
needed: string
) {
if (!ctx.scopes.includes(needed)) {
throw new Error(`Missing scope: ${needed}`);
}
}
假設你會在 MCP Gateway 驗證權杖後組出 RequestContext:解碼 JWT、檢查簽章/有效期、取出 sub、tenant、scope——然後將此脈絡附加到所有工具呼叫上。
接著在 tool handler 裡:
// mcp/tools/createOrder.ts
import { requireScope, RequestContext } from "../context";
export async function createOrder(
input: CreateOrderInput,
ctx: RequestContext
) {
requireScope(ctx, "orders:write");
// 接下來是建立訂單的邏輯
}
因此,即使模型在你預期之外的 UX 上下文中突然呼叫了 createOrder,沒有 orders:write 這個工具也不會執行。
在工具層級的 securitySchemes
MCP 規範允許每個工具宣告其需要的授權機制與 scopes。在官方範例中,securitySchemes 直接附在工具描述上。
範例:
// mcp/server.ts
server.registerTool(
"createOrder",
{
title: "Create order",
description: "Creates a new order for current user",
inputSchema: {/*...*/},
securitySchemes: [
{ type: "oauth2", scopes: ["orders:write"] }
]
},
async ({ input }, ctx: RequestContext) => {
requireScope(ctx, "orders:write");
// ...
}
);
這裡有兩層保護:
- 宣告式:ChatGPT 知道此工具需要 orders:write,若權限不足會啟動授權流程(或提示使用者);
- 命令式:你的程式碼在真正執行前再次檢查。
若有權杖但缺少必要的 scopes,伺服器應回傳帶有 WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write" 的錯誤——ChatGPT 便能向使用者請求擴權(step‑up authorization)。
Insight
官方範例中使用的 securitySchemes,其寫法在 ChatGPT Apps SDK 的範例裡出現,但尚未以該形式被正式規格所核准。因此需要標記為對官方協定的擴充——包在 _meta 中。上述範例的可行做法:
// mcp/server.ts
server.registerTool(
"createOrder",
{
title: "Create order",
description: "Creates a new order for current user",
inputSchema: {/*...*/},
_meta: { // 如此標註
securitySchemes: [
{ type: "oauth2", scopes: ["orders:write"] }
]
}
},
async ({ input }, ctx: RequestContext) => {
requireScope(ctx, "orders:write");
// ...
}
);
5. Per‑tool permissions 與「危險」工具
Scopes 回答的是「這個 auth_token 原則上可以做什麼」。但權杖中還包含模型可用的工具清單。這也需要謹慎設計。
工具分類
可將工具分成:
- 資訊型(informational/read‑only):讀取資料、產報表、做計算但沒有副作用;
- 具影響型(consequential):改變狀態、扣款、刪除資料等。
ChatGPT Apps 文件直接建議:對 read‑only 工具要明確標記其安全性;對危險工具要描述後果,並加入額外的 UX 確認。
可透過以下方式:
- 在工具上加註(如 readOnlyHint、destructiveHint);
- 在描述文字中說明:「此工具會不可逆地刪除訂單」;
- 使用獨立旗標 confirmation_required,讓你的 App 計畫在對話中插入確認步驟。
關鍵操作的 UX 確認
例如,GiftGenius 有個工具 chargeCustomer(啟動扣款)。你當然不希望模型在沒有使用者同意的情況下呼叫它。
在 App 計畫層可能是這樣:
// app/plan/tools.ts (偽程式碼)
export const tools = [
{
name: "giftgenius.list_catalog",
description: "顯示禮物目錄",
annotations: { readOnlyHint: true }
},
{
name: "giftgenius.create_order",
description: "建立未付款的訂單",
annotations: { consequential: true }
},
{
name: "giftgenius.charge_customer",
description: "為訂單扣款",
annotations: {
consequential: true,
destructiveHint: true,
confirmationRequired: {
title: "要從卡片扣款嗎?",
message: "將為訂單 N 進行付款。"
}
}
}
];
具體欄位名稱依 SDK 版本而異,但理念一致:read‑only 工具標記為安全;危險工具標示為需明確確認,並在描述中清楚解釋後果。
接著你的小工具可以回應:若模型提議呼叫 charge_customer,你就向使用者顯示一個語意清楚的對話框,只有在點擊「確認」後才真的進行 tool‑call。
小工具中的元件範例(簡化):
// widget/components/ConfirmCharge.tsx
export function ConfirmCharge(props: {
orderId: string;
onConfirm: () => void;
}) {
return (
<div>
<p>要為訂單 {props.orderId} 扣款嗎?</p>
<button onClick={props.onConfirm}>
是的,確認付款
</button>
</div>
);
}
模型負責提出「該付款了」的想法,但最後的按鈕由人來按。這就是安全團隊常說的 human‑in‑the‑loop。
僅供代理/後台使用的工具
另一個常見情境:有些工具只能由代理(指 Agents SDK)或內部管理介面使用,而不是「一般」的使用者向 ChatGPT App 使用。
例如 rebuildSearchIndex 或 syncCatalogFromERP。最好:
- 不要把它們放到一般 App 的工具清單中;
- 在獨立的代理/協調器中配置;
- 用獨立的 scopes 甚至獨立的 Auth 邏輯來保護。
若只是把它們加進 App 的可用工具清單,你就提高了模型突然「覺得」重建索引也許能幫助找禮物而直接動手的風險。
6. 網路分段與信任邊界
權限不只在於權杖上的 scopes。另一條重要軸線是網路與服務的分段。
理想狀態:
- 後端只有一個對外入口——MCP Gateway/Edge API;
- 所有存放 PII 與金流的服務都在私有網路/VPC 中,只能透過此閘道存取;
- 後端的對外連線(outbound)只允許到白名單域名(allowlist:金流、CRM、自家微服務)。
示意:
flowchart LR ChatGPT -- HTTPS --> Edge[API Gateway / MCP Endpoint] Edge -- private network --> MCP[MCP server] MCP -- private --> DB[(含 PII 的資料庫)] MCP -- private --> SVC[Internal microservices] MCP -- HTTPS (allow) --> Stripe[Payments API]
幾個關鍵規則:
- 資料庫與內部服務不直接暴露在網際網路上。只能從私有網路、且僅限真正需要的服務連入。
- Edge/Gateway 負責身分驗證與 rate‑limiting。由它檢查權杖與 scopes、限制過於頻繁的請求,並寫主要的稽核日誌。
- Egress 控制。MCP 伺服器不應能任意連往互聯網的任何 URL(避免 SSRF 與資料外洩)。外部主機清單最好明列限定。
實務上,若你把 MCP 部署到 Vercel、Render 或 Kubernetes 叢集,有些設定不一定能手動細調,但依然可以做到:
- dev/staging/prod 使用獨立專案/叢集;
- 各環境使用不同的環境變數與金鑰;
- 獨立的「edge」服務(MCP 的 HTTP 包裝)與獨立的私有服務。
總結一下,我們已經有兩條防線:權杖上的 scopes 與網路邊界。再加上第三條——多租戶(multi‑tenant),一個 App 服務多個組織。
7. Multi‑tenant/組織脈絡
前面我們假定只有單一使用者。但許多 ChatGPT 應用都是 multi‑tenant:同一個 App 服務許多公司。GiftGenius 很容易變成企業用的 B2B 服務:各部門有各自的目錄、預算、訂單。
什麼是 tenant,從哪裡取得
Tenant 通常指:
- 組織/公司(Acme Corp);
- 工作空間(workspace);
- 有時是專案或環境。
關鍵性質:一個 tenant 的資料不應對另一個 tenant 可見。
在 auth 流程中,tenant 通常放在:
- 權杖的 claim(tenant、org_id);
- 授權請求的獨立參數(但不如由 IdP 簽署的 claim 可靠)。
重點:只信任已驗證權杖中的 tenantId,而不是工具參數中的值。若模型生成了 {"tenantId": "acme"},但使用者權杖中是 tenantId: "globex",這應被視為入侵企圖。
請求脈絡中的 tenant
在我們的 RequestContext 中加入 tenantId(上文已加入),並且不能允許它被輸入資料覆寫。
基本檢查:
// mcp/tenant.ts
import { RequestContext } from "./context";
export function enforceTenant<TInput>(
input: TInput & { tenantId?: string },
ctx: RequestContext
) {
if (input.tenantId && input.tenantId !== ctx.tenantId) {
throw new Error("Tenant mismatch");
}
return { ...input, tenantId: ctx.tenantId };
}
接著在工具中:
// mcp/tools/listOrders.ts
export async function listOrders(
input: { limit?: number; tenantId?: string },
ctx: RequestContext
) {
const safe = enforceTenant(input, ctx);
return db.order.findMany({
where: { tenantId: safe.tenantId },
take: safe.limit ?? 20
});
}
我們忽略參數中的 tenant,強制從脈絡套入。如此一來,即使 LLM 或攻擊者試圖「塞」別人的 tenant,也不會奏效。
資料庫層的 tenant 隔離
架構上有多種方式:
- 每個 tenant 一個獨立資料庫;
- 獨立的 schema;
- 單一資料庫、各表包含 tenant_id,並做嚴格過濾。
無論你選哪種方式,金科玉律只有一條:任何對資料庫的查詢都不可不帶 tenant_id (取自脈絡) 的過濾條件。 在 RAG/向量搜尋中特別重要:若忘了按 tenant 過濾,模型可能會在其他組織的文件中查找。
8. 這如何落地到我們的 Next.js/Apps SDK 應用
現在把這些拼起來,看看 scopes、tenant 與網路邊界要如何在我們的 Next.js/Apps SDK 專案中落地。我們加點細節,看看 Next.js 與 Apps SDK 的程式碼。
在專案中的 scopes 與 tenant 位在哪裡
教學專案的典型配置:
- 在 Next.js 應用(Apps SDK)中,有 App/連接器設定與 OAuth callback 頁面;
- 在 MCP 伺服器中,有接收來自 ChatGPT 的 HTTP/SSE 請求、檢查權杖並呼叫對應工具的程式碼。
把上述要點落地:
- 在 MCP 資源的 OAuth 設定中宣告 GiftGenius 的 scopes_supported(catalog:read、orders:write 等)。
- 在 Apps SDK 設定中描述 App,列出工具與其註解(read‑only、consequential、confirmation‑flows)。
- 在 MCP 伺服器中實作:
- 權杖剖析與驗證;
- 組合 RequestContext:{ userId, tenantId, scopes };
- helper:requireScope、enforceTenant 等;
- 對資料庫的呼叫一律透過脈絡中的 tenantId。
建立訂單的「隔離」路徑範例
我們追蹤一個 end‑to‑end 的情境。
- 使用者寫道:「為這個套組在 50$ 預算內下單」。
- 模型判斷需要呼叫 giftgenius.create_order,並帶上參數 { productId, budget, ... }。
- ChatGPT 檢查 App 是否有 create_order 工具,以及為它宣告了哪些 scopes 與 securitySchemes。理解到需要 orders:write。
- 若已有權杖且包含 orders:write,則繼續;若沒有——ChatGPT 會以該 scope 啟動 OAuth 授權。
- MCP Gateway 接收請求、驗證權杖,組出 RequestContext: userId=123, tenantId="acme", scopes=["catalog:read","orders:write",...]。
- MCP 中的 createOrder:
- 執行 requireScope(ctx, "orders:write");
- 透過 enforceTenant 鎖定 tenant;
- 僅在 tenantId="acme" 的範圍內建立訂單。
- 若此訂單需要即時付款,模型或後端隨後會啟動 charge_customer,此處:
- 工具在計畫中標為 confirmationRequired;
- 小工具渲染 ConfirmCharge,請使用者明確確認扣款。
如此即可形成縱深防禦:過寬的 prompt、prompt 注入,甚至 UX 缺陷,都不會導致不受控的操作,因為在底層仍有嚴格的 scopes 檢查、tenant 約束與對關鍵行為的人為確認。
9. 設計權限與分段時的常見錯誤
錯誤一:像 app:full_access 這種「大而全」的 scope。
這種方式在 demo 時很方便,但在生產上很危險。丟了一把權杖——什麼都丟了。你無法只撤回或禁止某一操作而不影響其它。請按領域與操作類型拆分權限(read/write/critical)。
錯誤二:只在「入口」檢查權限,而不在工具內檢查。
有時會這樣做:「既然 ChatGPT 拿到了權杖,它就無所不能」。於是工具 createOrder 直接被呼叫,即使該權杖其實沒有 orders:write。正確作法是在每個工具中檢查 scopes(至少在所有會更動狀態的操作上以 middleware 集中檢查)。
錯誤三:未標示危險工具,也不要求確認。
若工具會扣款、刪除資料或改變存取權限,它不該在模型眼中與 listCatalog 一樣。缺乏明確的註記與 UX 確認,會提升模型基於「看起來合理」就去呼叫它的機率。至少請區分 read‑only 與 destructive 工具,並明確標示後者。
錯誤四:信任 tenantId 來自工具參數。
常見的反模式是:工具 getOrders({ tenantId }) 讓 tenantId 由模型傳入。若直接拿來用,tenantA 的使用者只要填別的識別,就可能讀到 tenantB 的資料。Tenant 必須來自已驗證的權杖,並強制套用在所有對資料庫與外部服務的請求上;使用者提供的值要嘛忽略、要嘛驗證一致性。
錯誤五:MCP/資料庫直接對外網開放。
有時在簡單原型裡,MCP 伺服器與資料庫就直接以 HTTP/5432 對外開著。在生產環境絕不可行:存取必須經由單一受保護的 gateway/proxy,資料庫則待在私有網路。否則任何被發現的脆弱 endpoint 或洞穴百出的 webhook,都是直通資料的捷徑。
錯誤六:在 dev 與 prod 使用相同的 scopes/密鑰。
最容易在展示本機 dev 環境時,意外刪掉生產資料的方式之一。每個環境都應有自己的金鑰、scopes 與資料庫。即使有人取得 dev 權杖,也傷不到 prod。
錯誤七:不願意「拒絕模型」。
有時開發者會擔心:「如果我常回 insufficient_scope 或 forbidden,模型會不會變笨?」實務上這是正常且預期的:模型會學到哪些行為可做、哪些需要更多授權或確認。更糟的是它「成功」做了本不該做的事——例如重複扣款。
GO TO FULL VERSION