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