Supabase MCP权限绕过漏洞分析与安全加固实践

Supabase MCP漏洞实录:权限绕过与加固实践
漏洞现场:全库SQL导出是怎么发生的
Hacker News上那条“Supabase MCP Server裸奔导出全库SQL”的帖子不是危言耸听。真实情况是:攻击者发一个特制的RPC请求,绕过所有权限检查,直接拿到pg_catalog里所有表结构、索引、视图定义,再拼出SELECT * FROM ...语句批量执行——整个数据库的SQL schema和数据就这么流出去了。
这不是协议缺陷,是Supabase的MCP Server实现漏掉了关键校验点。
权限校验在哪断了链
问题出在元数据接口的处理逻辑里。MCP规范要求每个RPC调用必须经过三道关卡:
- 调用者身份可信(JWT签名有效)
- 请求操作在授权范围内(如
read:tables) - 目标资源属于该调用者可访问的租户/项目(租户隔离)
但Supabase的list_tables和get_table_schema这两个接口跳过了第二、第三步。只要能连上Server,任何未认证或低权限用户都能调用它们。
结果就是:一个本该只读自己表的前端Agent,能查出整个PostgreSQL实例里所有数据库的全部表结构。
这不是MCP的锅,是实现没守规矩
MCP协议本身有明确的权限模型:每个RPC方法必须声明所需权限(required_permissions: ["read:tables"]),Server必须在执行前校验。沙箱机制也规定了Agent默认无权访问pg_catalog、无法跨数据库查询。
这次被“撞穿”的不是协议边界,而是开发者把协议当摆设——写了RPC handler,却忘了加if !ctx.HasPermission("read:tables") { return err }。
MCP安全机制:就两件事,别搞复杂
MCP的安全不靠玄学,靠两根柱子:权限控制和沙箱隔离。其他都是这两根柱子的延伸。
权限控制:每个RPC入口必须校验
MCP的权限是动词+名词组合:read:tables、write:rows、exec:sql。不是角色RBAC,是操作级ABAC。
关键规则:
- 每个注册的RPC handler开头必须做权限检查
- 权限检查必须包含租户上下文(
ctx.TenantID())和资源路径(req.Param("table_name")) - 拒绝时返回标准错误
mcp.ErrPermissionDenied,不暴露拒绝原因
server.Register("query", func(ctx mcp.Context, req mcp.Request) (mcp.Response, error) {
table := req.Param("table")
// 必须同时校验:权限 + 租户归属 + 表白名单
if !ctx.HasPermission("read:rows") ||
ctx.TenantID() != getTableTenant(table) ||
!isAllowedTable(table) {
return nil, mcp.ErrPermissionDenied
}
// ...
})沙箱隔离:资源访问必须显式授权
MCP沙箱不是Docker容器,是运行时约束:
- Agent进程默认无文件系统访问权限(
os.Open直接panic) - 网络只能连预注册的endpoint(如
supabase.com:5432) - 数据库连接自动注入租户schema前缀(
tenant_123.users)
违反规则的代码会当场失败:
// ❌ 这行会panic:未授权的文件访问
data, _ := os.ReadFile("/etc/passwd")
// ✅ 正确方式:通过MCP提供的安全API
content, err := ctx.ReadFile("config.yaml") // 仅限Agent工作目录Supabase漏洞复盘:两个硬伤
Supabase的修复补丁(v1.23.1)暴露了最初的问题根源:
1. 元数据接口完全裸奔
list_schemas、get_column_info这些方法注册时没声明任何权限要求:
// 错误示例(原始代码)
server.Register("list_schemas", listSchemasHandler) // 没配required_permissions导致MCP Server的全局权限中间件直接跳过校验。
2. 沙箱配置被手动绕过
为支持“管理员调试模式”,Supabase在启动时加了这个flag:
// 危险!生产环境绝对禁用
if os.Getenv("DEBUG_UNSAFE") == "true" {
disableSandbox()
}而某些部署文档里写着“设置DEBUG_UNSAFE=true启用高级功能”——于是沙箱形同虚设。
安全加固:三步落地
别谈理论,直接上能跑的代码。
第一步:强制权限校验中间件
在Server初始化时注入全局校验:
server.Use(func(next mcp.Handler) mcp.Handler {
return func(ctx mcp.Context, req mcp.Request) (mcp.Response, error) {
// 从RPC注册信息里读取声明的权限
perms := server.GetRequiredPermissions(req.Method())
for _, p := range perms {
if !ctx.HasPermission(p) {
return nil, mcp.ErrPermissionDenied
}
}
return next(ctx, req)
}
})第二步:沙箱策略硬编码
禁止运行时修改沙箱配置:
// 初始化时锁定沙箱
sandbox := mcp.NewSandbox()
sandbox.AllowNetwork("supabase.com:5432")
sandbox.AllowFileRead("config/", "secrets/") // 仅限指定目录
sandbox.Lock() // 后续调用disableSandbox()会panic第三步:最小权限原则落地
给每个Agent分配独立Token,权限精确到表:
{
"permissions": ["read:rows"],
"resources": ["users", "profiles"],
"tenant_id": "prod_abc123"
}而不是给前端Agent一个*:*通配符权限。
商业化前提:先守住底线
安全不是成本,是准入门槛。所有变现路径都建立在“用户信你不会丢数据”的基础上。
可落地的合规路径
- 按查询计费:Agent每次调用
query方法,Server记录tenant_id + table_name + row_count,生成账单。权限校验确保只计费被允许的查询。 - Agent市场审核:上架Agent必须通过沙箱扫描(检测是否调用
os/exec、net.Dial等危险API),权限声明必须匹配实际行为。 - 数据代理服务:用户授权后,Agent以
read:anonymized_rows权限运行,在内存中脱敏(如email字段替换为hash),再返回结果——全程不落盘。
下一步:动手改一行代码
现在打开你的MCP Server代码,找到任意一个RPC handler。在第一行插入:
if !ctx.HasPermission("your_required_permission") {
return nil, mcp.ErrPermissionDenied
}然后删掉所有DEBUG_UNSAFE环境变量相关逻辑。
做完这两件事,你就比Supabase早半年避开这个漏洞。