Supabase MCP Server鉴权加固指南:解决租户隔离与权限校验漏洞

MCP生态实战价值:Supabase漏洞反思与MCP Server鉴权加固
Supabase漏洞事件复盘
Hacker News上一篇关于“Supabase MCP漏洞可导出全库SQL”的讨论,实际指向一个更具体的问题:Supabase的MCP服务端未对/mcp/export端点做租户隔离和操作权限校验。攻击者只需构造一个合法认证头,就能触发全库导出,拿到所有表结构和数据。
这不是MCP协议本身的漏洞,而是Supabase在实现MCP Server时跳过了三个关键环节:
- 没绑定请求上下文到用户身份(例如没提取JWT中的
tenant_id或role字段) - 没校验操作语义(
export属于高危动作,不应开放给普通用户) - 没限制导出范围(默认导出全部schema,未强制指定
--schema=app_public之类约束)
这个案例暴露的不是协议缺陷,而是MCP Server开发中常见的“协议搬运工”陷阱:照搬协议定义,却忽略业务语义层的访问控制。
MCP Server必须守住的三道防线
MCP协议本身不规定鉴权模型,它只定义消息格式和路由规则。Server实现者必须自己补上这三层防护:
1. 上下文绑定:把请求锚定到真实身份
MCP请求里带Authorization头,但不能只验证token是否有效。要解析出sub、tenant_id、scope等声明,并在后续所有处理中显式传递。
# 正确做法:解析并注入上下文
from flask import g, request
import jwt
def auth_middleware():
token = request.headers.get('Authorization', '').replace('Bearer ', '')
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
g.user_id = payload['sub']
g.tenant_id = payload['tenant_id']
g.scopes = set(payload.get('scope', '').split())
except Exception:
return {'error': 'Invalid token'}, 401
@app.before_request
def before_request():
return auth_middleware()2. 动作分级:按操作敏感度动态授权
MCP操作不能一刀切。list_tools可以公开,export_db必须要求admin:tenant scope,delete_tool则需额外校验工具归属。
def require_scope(*required_scopes):
def decorator(f):
def wrapped(*args, **kwargs):
missing = set(required_scopes) - g.scopes
if missing:
return {'error': f'Missing scopes: {missing}'}, 403
# 额外检查:export_db必须限定schema
if 'export_db' in required_scopes and request.args.get('schema') != g.tenant_id:
return {'error': 'Export schema must match tenant'}, 403
return f(*args, **kwargs)
return wrapped
return decorator
@app.route('/mcp/export', methods=['POST'])
@require_scope('export_db')
def export_db():
# 此时g.tenant_id和scopes均已验证
return run_export(g.tenant_id)3. 数据沙箱:用数据库能力做物理隔离
别靠应用层if-else过滤数据。用PostgreSQL的Row Level Security(RLS)或MySQL的Schema级权限,让数据库自己拦住越权查询。
-- PostgreSQL RLS策略示例
ALTER TABLE public.tools ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON public.tools
USING (tenant_id = current_setting('app.current_tenant', true)::uuid);然后在应用层设置会话变量:
@app.before_request
def set_tenant_context():
db.execute("SET app.current_tenant = %s", (g.tenant_id,))这样即使代码漏掉某处校验,数据库也会拒绝返回其他租户的数据。
鉴权加固不是加功能,是改流程
很多团队把鉴权当成“加个中间件”,结果变成层层嵌套的装饰器。真正有效的加固,是把权限判断嵌入到核心路径里:
- 工具注册时,记录
owner_tenant_id - 请求解析后,立即查
tools表确认该工具属于当前租户 - 执行前,用
pg_advisory_xact_lock()对租户ID加事务锁,防并发冲突 - 日志里强制记录
tenant_id、user_id、action、duration_ms
@app.route('/mcp/execute', methods=['POST'])
def execute_tool():
tool_name = request.json['tool']
# 1. 查工具归属
tool = db.fetch_one("SELECT * FROM tools WHERE name = %s AND tenant_id = %s",
(tool_name, g.tenant_id))
if not tool:
return {'error': 'Tool not found or access denied'}, 404
# 2. 加租户锁(防并发修改配置)
db.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (g.tenant_id,))
# 3. 执行并计时
start = time.time()
result = run_tool(tool, request.json['args'])
duration = int((time.time() - start) * 1000)
# 4. 记录审计日志(含租户上下文)
audit_log.insert({
'tenant_id': g.tenant_id,
'user_id': g.user_id,
'action': 'execute_tool',
'tool': tool_name,
'duration_ms': duration,
'status': 'success' if result else 'failed'
})
return result商业化落地的关键:把安全设计变成客户能感知的价值
金融客户不关心你用了RBAC还是ABAC,但他们清楚知道:“如果你们的MCP Server被攻破,我的交易数据会不会流到竞争对手那里?”
所以商业化路径要倒过来推:
1. 从合规需求反推技术方案
- GDPR → 要求数据不出境 → 在MCP Server里加
region字段,路由时强制匹配 - HIPAA → 要求审计日志留存6年 → 把
audit_log表设为分区表,按月自动归档 - 等保三级 → 要求双因素登录 → 在token签发前,校验TOTP code
2. 把防护能力包装成可售特性
| 客户痛点 | 技术实现 | 产品话术 |
|---|---|---|
| 怕多租户数据混杂 | RLS + tenant_id强绑定 | “物理级租户隔离,通过PG官方RLS策略保障” |
| 怕误删生产数据 | DELETE操作需二次确认+72小时回收站 | “企业级操作保险箱,删除自动进回收站” |
| 怕API密钥泄露 | token支持细粒度scope + 1小时过期 | “最小权限令牌,支持按工具、按环境分发” |
3. 用客户自己的环境验证效果
别只讲PPT。给POC客户部署一个带审计看板的实例:
- 实时显示“今日拦截越权请求:17次”
- 展示一条被拒请求的完整链路:
JWT解析→scope校验失败→拒绝→写入审计日志 - 提供一键生成合规报告的按钮(含GDPR/HIPAA条款映射)
真实项目踩坑记录
我们给一家跨境支付公司上线MCP Server时,在/mcp/transfer接口栽过跟头:
- 初始版本只校验了
transfer_fundsscope,没校验from_account是否属于当前租户 - 攻击者用自己租户的token,把
from_account改成另一家客户的账户ID,成功转出资金 修复方案不是加if判断,而是:
- 在数据库加外键约束:
transfers.from_account_id → accounts.id+accounts.tenant_id = transfers.tenant_id - 查询时强制JOIN:
SELECT * FROM transfers t JOIN accounts a ON t.from_account_id = a.id AND a.tenant_id = %s - 所有account ID参数,统一走
uuid类型校验,防SQL注入
- 在数据库加外键约束:
这次事故让我们确认一条原则:鉴权逻辑必须下沉到数据访问层,应用层只做快速失败(fast fail),不承担最终裁决责任。
下一步行动清单
- [ ] 检查现有MCP Server所有端点,标记出
export、delete、exec类高危动作 - [ ] 给每个高危端点补上
tenant_id显式校验,删掉所有“全局查询”SQL - [ ] 在数据库启用RLS或对应隔离机制,用
EXPLAIN ANALYZE验证策略生效 - [ ] 把审计日志字段从
user_id扩展为{tenant_id,user_id,ip,ua},接入SIEM系统 - [ ] 更新客户文档,在“安全特性”章节直接写明:“所有导出操作强制限定schema,无法跨租户”