MCP協議安全邊界缺陷分析與Supabase漏洞加固指南

MCP協議安全邊界缺陷解析與加固指南:Supabase漏洞啟示錄
Supabase漏洞暴露了什么
Hacker News上熱議的“Supabase MCP漏洞致全庫SQL裸奔導出”事件,本質不是Supabase寫錯了代碼,而是MCP Server在默認配置下把數據庫當成了公共讀取器——任何能通過基礎認證的用戶,都能直接觸發/export端點,拿到整個PostgreSQL實例的SQL dump。
這個漏洞不依賴SQL注入、不靠服務端模板渲染,純粹是權限模型在協議層塌方的結果。它提醒我們:當AI Agent能自由調用MCP Server時,協議本身必須守住第一道門,而不是把所有信任都押在應用層的if語句上。
安全邊界在哪塌了
1. 默認配置等于開放大門
MCP Server啟動時不強制要求聲明資源策略。它的默認行為是:只要JWT簽名有效,就放行所有GET /resources/*和POST /actions/*請求。這不是疏忽,是協議設計選擇——但這個選擇在多租戶場景下立刻失效。
- 權限粒度缺失:MCP規范里沒有
resource_id、operation_type、context_scope等字段的強制校驗邏輯。Server實現通常只校驗sub(用戶ID)和exp(過期時間),剩下的全交給上層應用。 - 認證 ≠ 授權:Server完成身份認證后,直接把原始請求轉發給后端處理函數。如果那個函數沒做二次鑒權,或者鑒權邏輯被繞過(比如傳入
resource_id=*或table_name=public.*),數據就裸奔了。
Supabase案例中,攻擊者發送的請求類似:
POST /v1/export HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{"format": "sql", "schema": "public", "tables": ["*"]}MCP Server驗證完JWT就轉發,而Supabase的導出Handler沒對tables字段做白名單過濾,也沒檢查當前用戶是否有pg_dump權限。
2. 協議層零鑒權邏輯
MCP協議文檔明確將“授權決策”劃歸應用層責任。這導致兩個現實問題:
- 鑒權邏輯分散:同一個資源可能在API網關、MCP Server中間件、業務Handler里被校驗三次,也可能一次都沒被校驗——取決于開發者當天的心情。
- 請求可篡改無感知:MCP不強制簽名請求體,也不校驗
Content-MD5或X-Signature頭。攻擊者能輕松修改user_id、tenant_id、scope等關鍵參數,而Server照單轉發。
三步堵住協議層缺口
1. 在MCP Server層做鑒權,別甩鍋給應用
RBAC必須下沉到協議入口。不是加個中間件,而是讓MCP Server在解析完JWT后,立即查策略引擎,決定是否允許本次請求抵達業務Handler。
# mcp_server/middleware/authz.py
from policy_engine import evaluate
def enforce_mcp_authz(jwt_payload, method, path, body):
# 提取策略上下文
context = {
"user_id": jwt_payload["sub"],
"role": jwt_payload.get("role", "user"),
"method": method,
"path": path,
"resource": extract_resource_from_path(path),
"action": infer_action_from_method_and_path(method, path),
"body_keys": list(body.keys()) if isinstance(body, dict) else []
}
# 同步調用策略引擎(不走網絡,本地加載OPA/WASM)
if not evaluate("mcp_authz.rego", context):
raise PermissionDenied(f"Policy denied for {context}")
# 在請求路由前調用
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE"])
def handle_mcp_request(path):
jwt = parse_jwt(request.headers.get("Authorization"))
body = request.get_json() or {}
enforce_mcp_authz(jwt, request.method, path, body)
return dispatch_to_handler(path, body)2. 權限控制必須精確到操作+資源+上下文
“能訪問用戶表”不等于“能導出用戶表”。權限模型要區分:
read:user:profile(讀個人資料)read:table:users(讀users表所有行)export:database:public(導出整個public schema)
# 權限檢查邏輯必須嵌入業務Handler內部,而非僅依賴路由
def export_database_handler(user_id, schema, tables):
# 檢查用戶是否有該schema的導出權限
if not has_permission(user_id, f"export:schema:{schema}"):
raise Forbidden("Missing export:schema permission")
# 檢查每個table是否在用戶授權范圍內
allowed_tables = get_allowed_tables(user_id, schema)
for table in tables:
if table != "*" and table not in allowed_tables:
raise Forbidden(f"Table {table} not in allowed list")
# 執行導出(此時已確保安全)
return pg_dump(schema, tables)
# 權限數據存在獨立策略服務里,不和業務DB混用
def get_allowed_tables(user_id, schema):
return requests.get(
f"https://policy.internal/allowed-tables?user={user_id}&schema={schema}"
).json()3. Agent調用必須沙箱化,且沙箱由MCP Server管理
Agent不是可信執行體。MCP Server收到/agent/run請求后,不能直接subprocess.run(),而應:
- 啟動隔離容器(gVisor或Firecracker輕量VM)
- 掛載只讀的代碼目錄 + 臨時內存盤
- 設置
rlimit硬限制(CPU 5s、內存 256MB、網絡禁止外連) - 超時強殺,返回
SIGKILL狀態碼而非SIGTERM
# mcp_server/agent_runner.py
import firecracker
from tempfile import mkdtemp
def run_agent_sandboxed(agent_code, timeout=5):
# 創建臨時工作區
workdir = mkdtemp()
with open(f"{workdir}/main.py", "w") as f:
f.write(agent_code)
# 啟動Firecracker microVM
vm = firecracker.MicroVM(
kernel="/boot/vmlinux",
initrd="/rootfs.ext4",
cpu_count=1,
mem_size_mb=256,
network="none", # 禁止網絡
drives=[firecracker.Drive(workdir, readonly=True)]
)
try:
vm.start()
result = vm.execute("python3 /mnt/main.py", timeout=timeout)
return {"status": "success", "output": result.stdout}
except firecracker.TimeoutError:
vm.kill()
return {"status": "timeout", "error": "Execution exceeded 5s"}
finally:
vm.cleanup()安全是可交付的模塊,不是PPT里的形容詞
用戶不會為“高可用”付錢,但會為“導出數據前必須二次確認+審計日志+72小時追溯”付費。安全能力直接對應三個變現點:
- 企業版強制策略引擎:把OPA策略編譯成WASM,在MCP Server內聯執行,按策略條數收費
- 沙箱運行時即服務:按Agent調用次數和資源配額計費(如:100次/月免費,超量0.02美元/次)
- 合規審計包:自動生成SOC2、ISO27001所需日志視圖,附帶簽名報告
這些不是附加功能,是MCP Server的默認行為開關。關掉它們?可以,但得簽免責協議。
下一步:現在就做這五件事
- 停用所有
*通配符權限:檢查MCP Server配置,刪掉allow_all_authenticated這類flag - 在
/health端點旁加/policy-status:返回當前生效的策略版本、最后更新時間、未覆蓋路徑列表 - 給所有Agent調用加沙箱包裝器:哪怕只是
unshare -r -f --mount-proc也比裸跑強 - 重寫導出類API:強制要求
tables字段為非空數組,禁用["*"],改用list_tables接口分頁獲取 - 在CI里加策略測試:用真實JWT token跑
curl -X POST /export -d '{"tables":["users"]}',斷言返回403而非200