MCP协议Server鉴权加固实战:租户隔离与SQL执行安全合规方案
摘要:MCP协议实战解析:Server鉴权加固与合规商业化路径Supabase漏洞事件:一个真实的权限失控现场Hacker News上那篇题为“Supabase MCP漏洞可导出全库SQL”的帖子不是假设,是真实发生的越权读取。攻击者没用0day,只靠一个未校验租户上下文的/v1/sql端点,加上默认开启的pg_dump权限,就拿到了整个PostgreSQL实例的结构和数据。问题不在Supabas...
MCP协议实战解析:Server鉴权加固与合规商业化路径
Supabase漏洞事件:一个真实的权限失控现场
Hacker News上那篇题为“Supabase MCP漏洞可导出全库SQL”的帖子不是假设,是真实发生的越权读取。攻击者没用0day,只靠一个未校验租户上下文的/v1/sql端点,加上默认开启的pg_dump权限,就拿到了整个PostgreSQL实例的结构和数据。
问题不在Supabase本身,而在MCP Server实现层:它把“支持MCP协议”等同于“实现了MCP传输层”,却漏掉了最关键的租户隔离逻辑——没有在SQL执行前绑定current_user、没做schema级访问控制、也没校验请求头里的X-MCP-Tenant-ID是否匹配连接池中的实际租户。
这个漏洞暴露的不是MCP协议设计缺陷,而是Server开发者对协议语义的误读:MCP不定义权限模型,它只约定数据如何流动;权限必须由Server自己落地,且必须贯穿到每一行SQL、每一个HTTP响应头、每一次文件写入。
MCP Server权限控制的三个落地层
MCP协议文档里写的是“Server应确保多租户数据隔离”,但没说怎么确保。真正起作用的只有三层:
1. 连接层隔离(最基础也最容易被绕过)
- 每个租户必须使用独立数据库连接池,不能复用同一连接处理多个租户请求
- 连接建立时强制执行
SET search_path TO tenant_123, public,而非依赖应用层拼接schema前缀 - 使用
pgbouncer时禁用pool_mode = transaction,改用pool_mode = session
2. 查询层拦截(核心防线)
# 正确做法:在SQL解析阶段注入租户约束
def execute_sql(tenant_id: str, raw_sql: str) -> List[dict]:
# 1. 拦截DDL(禁止租户创建新schema)
if re.search(r'\b(CREATE|ALTER|DROP)\s+(SCHEMA|DATABASE)\b', raw_sql, re.I):
raise PermissionError("Tenant not allowed to manage schemas")
# 2. 自动重写SELECT语句,添加租户过滤
if raw_sql.strip().upper().startswith('SELECT'):
# 解析AST,找到FROM子句,对每个表注入WHERE tenant_id = ?
rewritten = inject_tenant_filter(raw_sql, tenant_id)
return run_query(rewritten, tenant_id)
# 3. 其他语句直接校验权限位
if not has_permission(tenant_id, 'sql_exec'):
raise PermissionError("SQL execution denied for tenant")3. 响应层净化(最后一道闸)
- 所有返回的JSON响应必须经过
sanitize_response()过滤,移除pg_stat_activity等系统视图中暴露的进程信息 - 文件导出(如CSV/SQL dump)必须用临时租户专用目录,路径格式为
/tmp/tenant_{id}_{uuid}/ - 错误消息禁用
debug=True,避免泄露表名、字段名、索引名
鉴权加固:从补丁到架构
Supabase事件后,我们重构了MCP Server的鉴权链,不再依赖单一Token校验,而是构建三级校验:
- 传输层校验:验证
Authorization: Bearer <JWT>签名与有效期 - 协议层校验:检查
X-MCP-Request-ID是否在白名单内(防重放),X-MCP-Client-Version是否兼容 - 业务层校验:根据
X-MCP-Tenant-ID查租户策略表,确认本次请求的操作类型(sql_exec,file_read,config_update)是否被授权
关键改动在tenant_policy表结构:
CREATE TABLE tenant_policy (
id SERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
operation TEXT NOT NULL CHECK (operation IN ('sql_exec', 'file_read', 'file_write', 'config_update')),
allowed BOOLEAN DEFAULT true,
max_rows INTEGER DEFAULT 10000,
timeout_ms INTEGER DEFAULT 5000,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 策略生效示例
INSERT INTO tenant_policy (tenant_id, operation, allowed, max_rows)
VALUES ('acme-corp', 'sql_exec', true, 50000);每次SQL执行前,查询该表并动态设置statement_timeout和LIMIT:
-- 在连接中执行
SET statement_timeout = 5000;
-- 在SQL末尾自动追加
-- LIMIT 50000商业化落地:一个Agent项目的硬核变现路径
我们做的跨云数据同步Agent,上线6个月后开始收费。没走“免费版限功能”路线,而是用安全能力倒逼付费:
收费锚点直指合规痛点
| 功能 | 免费版 | 付费版($299/月) |
|---|---|---|
| 租户数据隔离 | 进程级隔离 | 数据库实例级隔离 |
| SQL审计日志 | 仅记录成功操作 | 记录完整SQL+参数+执行计划 |
| 敏感字段脱敏 | 不支持 | 自动识别PII字段并AES加密 |
| 合规报告 | 无 | 自动生成SOC2/ISO27001模板 |
客户采购决策不是因为“功能多”,而是因为法务部明确要求:“生产环境必须满足租户间数据库实例隔离”。
关键转化动作
定价页不写功能列表,写合规声明:
“企业版部署即满足GDPR第32条‘适当技术措施’要求,所有租户数据存储于物理隔离的AWS RDS实例,VPC间无网络互通。”
- 免费试用限制真实场景:
免费用户只能连接本地PostgreSQL,无法添加AWS RDS或Cloud SQL连接——让客户在第一天就意识到“云环境需要企业版”。 - 销售话术聚焦故障成本:
“一次租户数据泄露的平均修复成本是$380万(IBM《2023数据泄露成本报告》)。企业版隔离方案的成本,不到一次事故的0.1%。”
下一步:把鉴权变成产品能力
别再把权限控制当成安全团队的补丁任务。把它做成可配置、可审计、可计费的产品模块:
- 在管理后台开放
tenant_policy编辑界面,让客户自行开关file_read权限 - 将SQL审计日志接入客户现有SIEM系统(Splunk/Sentinel),作为增值集成项收费
- 对高频查询自动触发
EXPLAIN ANALYZE,生成性能报告——按报告份数收取分析费
真正的商业化,始于把安全约束翻译成客户愿意付费的确定性。