🧩 MCP生态

Supabase MCP插件权限校验漏洞深度解析:防范数据库裸奔导出风险

发布时间:2026-04-14 分类: MCP生态
摘要:Supabase MCP插件漏洞事件深度解析:如何避免“裸奔导出”风险漏洞本质:权限校验被跳过,数据库直接暴露Supabase MCP插件存在一个高危缺陷:它在处理MCP请求时,没有强制验证能力声明(capability)是否真实来自可信令牌。攻击者只需构造一个带伪造 capability 声明的 HTTP 请求(例如,手动设置 Authorization: Bearer ... 并篡改 p...

封面

Supabase MCP插件漏洞事件深度解析:如何避免“裸奔导出”风险

漏洞本质:权限校验被跳过,数据库直接暴露

Supabase MCP插件存在一个高危缺陷:它在处理MCP请求时,没有强制验证能力声明(capability)是否真实来自可信令牌。攻击者只需构造一个带伪造 capability 声明的 HTTP 请求(例如,手动设置 Authorization: Bearer ... 并篡改 payload),就能绕过所有权限检查,直连 PostgreSQL 实例并执行 pg_dump 级别的全量导出。

这不是配置错误,而是代码逻辑缺失——插件把 capability 当作输入参数直接信任,没做签名验证、作用域比对或上下文绑定。

MCP协议的关键约束,不是装饰

MCP 协议本身不自动提供安全。它的机制只有在被严格执行时才起作用:

  • 能力声明(Capability Declaration)
    是 JSON 对象,含 namedescriptionparameterspermissions 字段。permissions 字段必须明确列出所需数据库权限(如 "supabase_read:public.users")。但声明本身无加密或签名,必须由服务端用令牌重新推导并校验,不能直接信任客户端传入的值。
  • 权限沙箱机制(Permission Sandbox)
    不是进程隔离,而是运行时数据访问控制。例如,一个声明了 "supabase_read:public.orders" 的 capability,对应的实际数据库查询必须被硬编码限制在 orders 表、只读、且自动注入 WHERE tenant_id = ?(如果多租户)。沙箱失效的根源,常在于 ORM 层未拦截原始 SQL 执行。
  • 认证与授权(Authentication and Authorization)
    MCP 要求每个请求携带 JWT。该 token 必须由可信签发方(如 Supabase Auth)生成,payload 中需包含 sub(用户 ID)、role(PostgreSQL 角色)、permissions(预计算的权限列表)和 exp。插件必须调用 supabase.auth.getUser() 或等效接口解码并验证 token,而不是仅检查 Authorization 头是否存在。

漏洞复现路径(精简版)

  1. 攻击者用 Postman 发送请求:

    POST /mcp/execute HTTP/1.1
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    Content-Type: application/json
    
    {
      "capability": {
        "name": "export_full_db",
        "permissions": ["supabase_read:*"]
      },
      "operation": "dump"
    }
  2. 插件解析 capability.permissions,发现 supabase_read:*,直接允许执行导出逻辑。
  3. 后端调用 pg_dump --dbname=... --format=custom,无租户过滤、无行级安全(RLS)绕过检查、无角色切换(仍以 postgresservice_role 运行)。

根本问题:插件把 capability 当作“指令”,而非“声明”;把 token 当作“凭证”,而非“权威来源”。

防御性编码:四条硬规则

  1. 能力声明必须二次派生,不可信任客户端输入
    删除所有直接解析请求体中 capability.permissions 的逻辑。改为从 JWT 中提取 permissions 字段,并与当前请求的操作做精确匹配:

    def handle_request(request):
        token = request.headers.get("Authorization", "").replace("Bearer ", "")
        if not token:
            return {"error": "missing token"}, 401
    
        try:
            payload = jwt.decode(token, SUPABASE_JWT_SECRET, algorithms=["HS256"])
        except jwt.InvalidTokenError:
            return {"error": "invalid token"}, 401
    
        # 从 token 中取权限,不是从 request.body
        allowed_perms = payload.get("permissions", [])
        required_perm = f"supabase_read:{request.table}"
    
        if required_perm not in allowed_perms and "supabase_read:*" not in allowed_perms:
            return {"error": "permission denied"}, 403
    
        # 继续执行,但必须用受限角色连接 DB
        return execute_dump(request.table)
  2. 数据库连接必须降权
    即使 token 有效,后端连接 PostgreSQL 时,绝不能使用 service_rolepostgres 用户。应为每个租户或每个能力组创建最小权限角色,并在连接时动态 SET ROLE

    -- 创建只读角色
    CREATE ROLE mcp_read_only NOINHERIT;
    GRANT CONNECT ON DATABASE mydb TO mcp_read_only;
    GRANT USAGE ON SCHEMA public TO mcp_read_only;
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_read_only;
    ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_read_only;

    Python 中连接后立即执行:

    conn.cursor().execute("SET ROLE mcp_read_only")
  3. 禁止原始 SQL 导出,改用受控快照
    pg_dump 是反模式。正确做法是:

    • 提前定义可导出的表清单(白名单)
    • 对每张表执行 COPY (SELECT * FROM table WHERE tenant_id = %s) TO STDOUT WITH CSV
    • 输出流经内存缓冲,不写磁盘,超时强制中断
    • 导出文件名强制带时间戳和哈希,不暴露原始表名
  4. 日志必须记录权限决策链
    记录不能只记“谁访问了什么”,要记“为什么允许/拒绝”:

    logger.info(
        "mcp_request",
        extra={
            "user_id": payload["sub"],
            "requested_table": request.table,
            "token_permissions": payload.get("permissions", []),
            "allowed_by": "supabase_read:public.users" in payload.get("permissions", []),
            "status": "allowed" if allowed else "denied"
        }
    )

商业化切口:合规数据代理的真实机会

漏洞暴露的恰恰是市场缺口——企业需要有人替他们管住 Agent 的手。

一个可行的落地场景:

  • 某 SaaS 公司有 200 家客户,每家数据隔离在独立 schema(tenant_123)。
  • 他们想让客服 Agent 查询客户订单,但怕 Agent 写错 SQL 泄露其他租户数据。

你的 Agent 服务不是“通用 MCP 网关”,而是:

  • Schema-aware 代理:收到 SELECT * FROM orders 时,自动重写为 SELECT * FROM tenant_123.orders,且校验 tenant_123 是否属于当前 token 的 tenant_id 字段。
  • 字段级脱敏开关:配置 orders.credit_card_last4 字段对客服角色始终返回 ****,无需修改业务代码。
  • 审计水印:所有导出 CSV 自动追加一行 # exported_by:agent-v2.1|tenant:123|timestamp:2024-05-22T08:30Z

收费模型更实际:

  • $300/月/租户(按实际接入租户数计费,非按 Agent 数量)
  • $1500 一次性配置费(含 RLS 规则审查 + 自动 schema 注入脚本)
  • 导出操作按次计费($0.02/次),抑制滥用

关键不是卖技术,是卖“责任转移”——你签 SLA,承诺数据不出界;他们省去内部安全团队逐行审代码。

部署 checklist:三步堵死漏洞

  1. 删掉所有 capability 解析逻辑
    搜索代码库中的 request.json.get("capability")req.body.capability,全部删除。权限只从 JWT 来。
  2. 强制连接降权
    在数据库连接池初始化时,显式设置 options="-c role=mcp_read_only"(libpq)或 connection_options={"options": "-c role=mcp_read_only"}(asyncpg)。测试时用 SELECT current_user, session_user, current_role 验证。
  3. 上线前跑通这三条命令

    # 1. 确保无 service_role 连接
    psql -c "SELECT * FROM pg_stat_activity WHERE usename = 'service_role'"
    
    # 2. 确保 RLS 对所有敏感表启用
    psql -c "SELECT schemaname, tablename, relrowsecurity FROM pg_tables WHERE schemaname = 'public' AND relrowsecurity = false"
    
    # 3. 模拟攻击:用无效 token 请求,确认返回 401/403,不是 500 或数据
    curl -H "Authorization: Bearer invalid" https://your-api/mcp/export

漏洞修复不是加补丁,是重校准信任边界:JWT 是唯一信源,数据库角色是唯一执行主体,日志是唯一证据链。

返回首页