Supabase MCP插件安全漏洞解析:協(xié)議層鑒權(quán)缺失導(dǎo)致SQL直連風(fēng)險
Supabase MCP插件安全隱患:開源不等于安全,協(xié)議層鑒權(quán)缺失才是關(guān)鍵
問題本質(zhì):MCP endpoint 沒有協(xié)議級防護
Supabase 的 MCP 插件暴露了一個 /mcp endpoint,它直接將客戶端傳入的 SQL 查詢轉(zhuǎn)發(fā)給數(shù)據(jù)庫執(zhí)行。這個設(shè)計跳過了所有應(yīng)用層訪問控制——沒有用戶身份校驗、沒有權(quán)限分級、沒有操作白名單。只要能發(fā) HTTP 請求到這個地址,就能讀取任意表、導(dǎo)出全庫、甚至刪庫。
這不是配置疏忽,是協(xié)議實現(xiàn)層面的缺失:MCP 規(guī)范本身要求服務(wù)端對每個操作做鑒權(quán)和沙箱約束,但當前插件把這部分完全交給了上層應(yīng)用,而多數(shù)開發(fā)者根本沒意識到這里需要補漏。
協(xié)議層該有的兩道防線
鑒權(quán)不是可選項
MCP 協(xié)議明確要求服務(wù)端驗證調(diào)用方身份,并基于角色或策略限制其可執(zhí)行的操作類型(如只讀、只查特定表)。Supabase 插件目前把 Authorization 頭直接忽略,所有請求都以數(shù)據(jù)庫超級用戶身份執(zhí)行。
后果很直接:
- 攻擊者用
curl -X POST https://your-project.supabase.co/mcp -d '{"query":"SELECT * FROM users"}'就能拿到全部用戶數(shù)據(jù) - 如果數(shù)據(jù)庫連接配置了高權(quán)限賬號,
DROP TABLE、COPY TO PROGRAM等危險操作也能成功
沙箱不是性能負擔,是必要隔離
沙箱機制要解決兩個問題:
- 語法隔離:禁止
DELETE/UPDATE/INSERT/TRUNCATE等寫操作,只允許SELECT - 語義隔離:限制
SELECT范圍,比如禁止跨 schema 查詢、禁止information_schema探針、限制返回行數(shù)
當前插件不做任何解析,原樣執(zhí)行傳入的 SQL 字符串。一個 UNION SELECT pg_read_file('/etc/passwd') 就可能觸發(fā)數(shù)據(jù)庫側(cè)信道泄露。
實際風(fēng)險場景
全庫導(dǎo)出無需提權(quán)
攻擊者構(gòu)造如下請求即可導(dǎo)出整個數(shù)據(jù)庫結(jié)構(gòu)和內(nèi)容:
curl -X POST https://your-project.supabase.co/mcp \
-H "Content-Type: application/json" \
-d '{
"query": "SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\'"
}'拿到表名后,循環(huán)執(zhí)行 SELECT * FROM <table>,幾條 shell 腳本就能完成全量 dump。Supabase 默認開啟 Row Level Security(RLS),但 MCP 插件繞過了 RLS——它直連數(shù)據(jù)庫,不走 PostgREST 層。
敏感數(shù)據(jù)在 AI 流水線中裸奔
很多 AI Agent 項目用 MCP 插件做“數(shù)據(jù)庫工具調(diào)用”,把用戶會話 ID、支付記錄、原始日志等直接喂給 LLM。一旦 MCP endpoint 泄露,這些數(shù)據(jù)就變成明文 CSV 流向外部。
更危險的是:LLM 可能被誘導(dǎo)生成惡意查詢。比如用戶輸入“幫我刪掉測試數(shù)據(jù)”,Agent 解析成 DELETE FROM logs WHERE is_test = true 并提交——插件照單執(zhí)行,且無審計日志。
寫操作導(dǎo)致服務(wù)雪崩
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'attacker' 這類語句不會報錯,但會污染業(yè)務(wù)數(shù)據(jù)。更糟的是 VACUUM FULL 或 CREATE INDEX CONCURRENTLY 等維護命令可能鎖表,讓整個應(yīng)用不可用。
怎么修:在 endpoint 層加兩道硬閘
必須加鑒權(quán):JWT 校驗 + 權(quán)限映射
不要依賴網(wǎng)絡(luò)層防火墻或 IP 白名單。每個請求必須攜帶有效 JWT,且 token payload 中需聲明 allowed_tables 和 allowed_operations。
from flask import Flask, request, jsonify
import jwt
from datetime import datetime
app = Flask(__name__)
def require_mcp_auth(f):
def decorated(*args, **kwargs):
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid Authorization header'}), 401
token = auth[7:]
try:
payload = jwt.decode(token, 'your-supabase-jwt-secret', algorithms=['HS256'])
# 強制檢查權(quán)限字段
if 'allowed_tables' not in payload or 'allowed_operations' not in payload:
return jsonify({'error': 'Invalid token: missing permissions'}), 403
request.mcp_permissions = payload
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
@app.route('/mcp', methods=['POST'])
@require_mcp_auth
def mcp_handler():
data = request.get_json()
query = data.get('query', '').strip()
# 后續(xù)校驗 query 是否符合 permissions...必須加沙箱:SQL 解析 + 白名單執(zhí)行
別用字符串拼接或正則匹配禁用關(guān)鍵詞——%00DELETE、注釋繞過、Unicode 變體都能繞過。用真正的 SQL 解析器(如 sqlparse)提取 AST,只允許 SELECT 類型節(jié)點,且 FROM 子句中的表名必須在 allowed_tables 列表中。
import sqlparse
from sqlparse.sql import IdentifierList, Identifier, Where, Comparison
from sqlparse.tokens import Keyword, DML
def validate_query(query: str, allowed_tables: list) -> bool:
parsed = sqlparse.parse(query)[0]
# 檢查是否為 SELECT
if not parsed.token_first().ttype is DML and parsed.token_first().value.upper() != 'SELECT':
return False
# 提取所有表名
tables = set()
for token in parsed.flatten():
if isinstance(token, Identifier) and token.has_alias():
tables.add(token.get_real_name())
elif isinstance(token, IdentifierList):
for identifier in token.get_identifiers():
if hasattr(identifier, 'get_real_name'):
tables.add(identifier.get_real_name())
# 檢查表名是否都在白名單中
return all(table in allowed_tables for table in tables)
@app.route('/mcp', methods=['POST'])
@require_mcp_auth
def mcp_handler():
data = request.get_json()
query = data.get('query', '').strip()
if not validate_query(query, request.mcp_permissions['allowed_tables']):
return jsonify({'error': 'Query violates table access policy'}), 403
# 執(zhí)行查詢(使用參數(shù)化,避免二次注入)
with db_engine.connect() as conn:
result = conn.execute(text(query))
return jsonify([dict(row) for row in result]), 200真實項目怎么落地
不要自己重寫 MCP Server
Supabase 官方插件的問題在于它把協(xié)議實現(xiàn)簡化成了“HTTP → SQL”管道。更穩(wěn)妥的做法是:
- 用 MCP SDK 啟動標準 server
- 在
tool_callhandler 中注入鑒權(quán)邏輯 - 所有數(shù)據(jù)庫操作走 Supabase Client(自動帶 RLS)而非直連
這樣既能復(fù)用社區(qū)規(guī)范,又把權(quán)限控制收回到應(yīng)用層。
關(guān)鍵檢查清單
部署前確認以下五點:
- ? MCP endpoint 的數(shù)據(jù)庫連接賬號權(quán)限最小化(只讀 + 僅限必要 schema)
- ? JWT secret 與 Supabase 項目密鑰分離,不硬編碼在代碼里
- ?
allowed_tables按業(yè)務(wù)場景動態(tài)生成(如客服 Agent 只能查tickets和users) - ? 所有
SELECT查詢自動加上LIMIT 1000,防止大結(jié)果集拖垮內(nèi)存 - ? 日志記錄每次 MCP 調(diào)用的 token subject、表名、查詢哈希(不記原始 SQL)
商業(yè)項目必須簽 DPA
如果你的 Agent 服務(wù)把客戶數(shù)據(jù)傳給第三方 MCP provider,必須簽數(shù)據(jù)處理協(xié)議(DPA),明確:
- provider 不得存儲原始數(shù)據(jù)
- 查詢?nèi)罩颈A舨怀^ 7 天
- 審計權(quán)開放給客戶(提供 API 查看調(diào)用記錄)
- 違約賠償條款(例如每條泄露記錄罰 $500)
GDPR 和國內(nèi)《個人信息保護法》都把這種“通過協(xié)議接口傳輸數(shù)據(jù)”的行為認定為委托處理,責(zé)任主體仍是你的產(chǎn)品方。
下一步動作
- 立即禁用生產(chǎn)環(huán)境的 Supabase MCP 插件,改用 PostgREST + 自定義函數(shù)封裝數(shù)據(jù)庫操作
- 把
supabase-mcp倉庫 fork 出來,在src/server.ts的handleMcpRequest函數(shù)里插入鑒權(quán)和 SQL 白名單邏輯 - 用 sqlglot 替代正則做 SQL 解析——它支持 Postgres 語法樹,能準確識別 CTE、子查詢、函數(shù)調(diào)用等復(fù)雜結(jié)構(gòu)
安全不是加個中間件,是把權(quán)限決策點放在協(xié)議入口處。MCP 的價值在于標準化,但標準化的前提是每個實現(xiàn)都守住底線。