🧩 MCP生态

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

发布时间:2026-04-16 分类: MCP生态
摘要:Supabase MCP漏洞实录:权限绕过与加固实践漏洞现场:全库SQL导出是怎么发生的Hacker News上那条“Supabase MCP Server裸奔导出全库SQL”的帖子不是危言耸听。真实情况是:攻击者发一个特制的RPC请求,绕过所有权限检查,直接拿到pg_catalog里所有表结构、索引、视图定义,再拼出SELECT * FROM ...语句批量执行——整个数据库的SQL sc...

封面

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_tablesget_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:tableswrite:rowsexec: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_schemasget_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/execnet.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早半年避开这个漏洞。

返回首页