Supabase MCP插件權(quán)限校驗(yàn)漏洞深度解析:防范數(shù)據(jù)庫(kù)裸奔導(dǎo)出風(fēng)險(xiǎn)

Supabase MCP插件漏洞事件深度解析:如何避免“裸奔導(dǎo)出”風(fēng)險(xiǎn)
漏洞本質(zhì):權(quán)限校驗(yàn)被跳過(guò),數(shù)據(jù)庫(kù)直接暴露
Supabase MCP插件存在一個(gè)高危缺陷:它在處理MCP請(qǐng)求時(shí),沒(méi)有強(qiáng)制驗(yàn)證能力聲明(capability)是否真實(shí)來(lái)自可信令牌。攻擊者只需構(gòu)造一個(gè)帶偽造 capability 聲明的 HTTP 請(qǐng)求(例如,手動(dòng)設(shè)置 Authorization: Bearer ... 并篡改 payload),就能繞過(guò)所有權(quán)限檢查,直連 PostgreSQL 實(shí)例并執(zhí)行 pg_dump 級(jí)別的全量導(dǎo)出。
這不是配置錯(cuò)誤,而是代碼邏輯缺失——插件把 capability 當(dāng)作輸入?yún)?shù)直接信任,沒(méi)做簽名驗(yàn)證、作用域比對(duì)或上下文綁定。
MCP協(xié)議的關(guān)鍵約束,不是裝飾
MCP 協(xié)議本身不自動(dòng)提供安全。它的機(jī)制只有在被嚴(yán)格執(zhí)行時(shí)才起作用:
- 能力聲明(Capability Declaration)
是 JSON 對(duì)象,含name、description、parameters和permissions字段。permissions字段必須明確列出所需數(shù)據(jù)庫(kù)權(quán)限(如"supabase_read:public.users")。但聲明本身無(wú)加密或簽名,必須由服務(wù)端用令牌重新推導(dǎo)并校驗(yàn),不能直接信任客戶端傳入的值。 - 權(quán)限沙箱機(jī)制(Permission Sandbox)
不是進(jìn)程隔離,而是運(yùn)行時(shí)數(shù)據(jù)訪問(wèn)控制。例如,一個(gè)聲明了"supabase_read:public.orders"的 capability,對(duì)應(yīng)的實(shí)際數(shù)據(jù)庫(kù)查詢必須被硬編碼限制在orders表、只讀、且自動(dòng)注入WHERE tenant_id = ?(如果多租戶)。沙箱失效的根源,常在于 ORM 層未攔截原始 SQL 執(zhí)行。 - 認(rèn)證與授權(quán)(Authentication and Authorization)
MCP 要求每個(gè)請(qǐng)求攜帶 JWT。該 token 必須由可信簽發(fā)方(如 Supabase Auth)生成,payload 中需包含sub(用戶 ID)、role(PostgreSQL 角色)、permissions(預(yù)計(jì)算的權(quán)限列表)和exp。插件必須調(diào)用supabase.auth.getUser()或等效接口解碼并驗(yàn)證 token,而不是僅檢查Authorization頭是否存在。
漏洞復(fù)現(xiàn)路徑(精簡(jiǎn)版)
攻擊者用 Postman 發(fā)送請(qǐng)求:
POST /mcp/execute HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Content-Type: application/json { "capability": { "name": "export_full_db", "permissions": ["supabase_read:*"] }, "operation": "dump" }- 插件解析
capability.permissions,發(fā)現(xiàn)supabase_read:*,直接允許執(zhí)行導(dǎo)出邏輯。 - 后端調(diào)用
pg_dump --dbname=... --format=custom,無(wú)租戶過(guò)濾、無(wú)行級(jí)安全(RLS)繞過(guò)檢查、無(wú)角色切換(仍以postgres或service_role運(yùn)行)。
根本問(wèn)題:插件把 capability 當(dāng)作“指令”,而非“聲明”;把 token 當(dāng)作“憑證”,而非“權(quán)威來(lái)源”。
防御性編碼:四條硬規(guī)則
能力聲明必須二次派生,不可信任客戶端輸入
刪除所有直接解析請(qǐng)求體中capability.permissions的邏輯。改為從 JWT 中提取permissions字段,并與當(dāng)前請(qǐng)求的操作做精確匹配:def handle_request(request): token = request.headers.get("Authorization", "").replace("Bearer ", "") if not token: return {"error": "missing token"}, 401 try: payload = jwt.decode(token, SUPABASE_JWT_SECRET, algorithms=["HS256"]) except jwt.InvalidTokenError: return {"error": "invalid token"}, 401 # 從 token 中取權(quán)限,不是從 request.body allowed_perms = payload.get("permissions", []) required_perm = f"supabase_read:{request.table}" if required_perm not in allowed_perms and "supabase_read:*" not in allowed_perms: return {"error": "permission denied"}, 403 # 繼續(xù)執(zhí)行,但必須用受限角色連接 DB return execute_dump(request.table)數(shù)據(jù)庫(kù)連接必須降權(quán)
即使 token 有效,后端連接 PostgreSQL 時(shí),絕不能使用service_role或postgres用戶。應(yīng)為每個(gè)租戶或每個(gè)能力組創(chuàng)建最小權(quán)限角色,并在連接時(shí)動(dòng)態(tài)SET ROLE:-- 創(chuàng)建只讀角色 CREATE ROLE mcp_read_only NOINHERIT; GRANT CONNECT ON DATABASE mydb TO mcp_read_only; GRANT USAGE ON SCHEMA public TO mcp_read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_read_only; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_read_only;Python 中連接后立即執(zhí)行:
conn.cursor().execute("SET ROLE mcp_read_only")禁止原始 SQL 導(dǎo)出,改用受控快照
pg_dump是反模式。正確做法是:- 提前定義可導(dǎo)出的表清單(白名單)
- 對(duì)每張表執(zhí)行
COPY (SELECT * FROM table WHERE tenant_id = %s) TO STDOUT WITH CSV - 輸出流經(jīng)內(nèi)存緩沖,不寫磁盤,超時(shí)強(qiáng)制中斷
- 導(dǎo)出文件名強(qiáng)制帶時(shí)間戳和哈希,不暴露原始表名
日志必須記錄權(quán)限決策鏈
記錄不能只記“誰(shuí)訪問(wèn)了什么”,要記“為什么允許/拒絕”:logger.info( "mcp_request", extra={ "user_id": payload["sub"], "requested_table": request.table, "token_permissions": payload.get("permissions", []), "allowed_by": "supabase_read:public.users" in payload.get("permissions", []), "status": "allowed" if allowed else "denied" } )
商業(yè)化切口:合規(guī)數(shù)據(jù)代理的真實(shí)機(jī)會(huì)
漏洞暴露的恰恰是市場(chǎng)缺口——企業(yè)需要有人替他們管住 Agent 的手。
一個(gè)可行的落地場(chǎng)景:
- 某 SaaS 公司有 200 家客戶,每家數(shù)據(jù)隔離在獨(dú)立 schema(
tenant_123)。 - 他們想讓客服 Agent 查詢客戶訂單,但怕 Agent 寫錯(cuò) SQL 泄露其他租戶數(shù)據(jù)。
你的 Agent 服務(wù)不是“通用 MCP 網(wǎng)關(guān)”,而是:
- Schema-aware 代理:收到
SELECT * FROM orders時(shí),自動(dòng)重寫為SELECT * FROM tenant_123.orders,且校驗(yàn)tenant_123是否屬于當(dāng)前 token 的tenant_id字段。 - 字段級(jí)脫敏開關(guān):配置
orders.credit_card_last4字段對(duì)客服角色始終返回****,無(wú)需修改業(yè)務(wù)代碼。 - 審計(jì)水印:所有導(dǎo)出 CSV 自動(dòng)追加一行
# exported_by:agent-v2.1|tenant:123|timestamp:2024-05-22T08:30Z。
收費(fèi)模型更實(shí)際:
- $300/月/租戶(按實(shí)際接入租戶數(shù)計(jì)費(fèi),非按 Agent 數(shù)量)
- $1500 一次性配置費(fèi)(含 RLS 規(guī)則審查 + 自動(dòng) schema 注入腳本)
- 導(dǎo)出操作按次計(jì)費(fèi)($0.02/次),抑制濫用
關(guān)鍵不是賣技術(shù),是賣“責(zé)任轉(zhuǎn)移”——你簽 SLA,承諾數(shù)據(jù)不出界;他們省去內(nèi)部安全團(tuán)隊(duì)逐行審代碼。
部署 checklist:三步堵死漏洞
- 刪掉所有
capability解析邏輯
搜索代碼庫(kù)中的request.json.get("capability")、req.body.capability,全部刪除。權(quán)限只從 JWT 來(lái)。 - 強(qiáng)制連接降權(quán)
在數(shù)據(jù)庫(kù)連接池初始化時(shí),顯式設(shè)置options="-c role=mcp_read_only"(libpq)或connection_options={"options": "-c role=mcp_read_only"}(asyncpg)。測(cè)試時(shí)用SELECT current_user, session_user, current_role驗(yàn)證。 上線前跑通這三條命令
# 1. 確保無(wú) service_role 連接 psql -c "SELECT * FROM pg_stat_activity WHERE usename = 'service_role'" # 2. 確保 RLS 對(duì)所有敏感表啟用 psql -c "SELECT schemaname, tablename, relrowsecurity FROM pg_tables WHERE schemaname = 'public' AND relrowsecurity = false" # 3. 模擬攻擊:用無(wú)效 token 請(qǐng)求,確認(rèn)返回 401/403,不是 500 或數(shù)據(jù) curl -H "Authorization: Bearer invalid" https://your-api/mcp/export
漏洞修復(fù)不是加補(bǔ)丁,是重校準(zhǔn)信任邊界:JWT 是唯一信源,數(shù)據(jù)庫(kù)角色是唯一執(zhí)行主體,日志是唯一證據(jù)鏈。