🧩 MCP生态

MCP协议安全边界缺陷分析与Supabase漏洞加固指南

发布时间:2026-04-17 分类: MCP生态
摘要:MCP协议安全边界缺陷解析与加固指南:Supabase漏洞启示录Supabase漏洞暴露了什么Hacker News上热议的“Supabase MCP漏洞致全库SQL裸奔导出”事件,本质不是Supabase写错了代码,而是MCP Server在默认配置下把数据库当成了公共读取器——任何能通过基础认证的用户,都能直接触发/export端点,拿到整个PostgreSQL实例的SQL dump。这...

封面

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_idoperation_typecontext_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-MD5X-Signature头。攻击者能轻松修改user_idtenant_idscope等关键参数,而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的默认行为开关。关掉它们?可以,但得签免责协议。

下一步:现在就做这五件事

  1. 停用所有*通配符权限:检查MCP Server配置,删掉allow_all_authenticated这类flag
  2. /health端点旁加/policy-status:返回当前生效的策略版本、最后更新时间、未覆盖路径列表
  3. 给所有Agent调用加沙箱包装器:哪怕只是unshare -r -f --mount-proc也比裸跑强
  4. 重写导出类API:强制要求tables字段为非空数组,禁用["*"],改用list_tables接口分页获取
  5. 在CI里加策略测试:用真实JWT token跑curl -X POST /export -d '{"tables":["users"]}',断言返回403而非200
返回首页