MCP协议安全配置指南:防范Supabase式数据库暴露的实战要点
摘要:MCP协议安全:避免Supabase式数据库暴露的实战要点Supabase事件复盘:不是“漏洞”,是配置与边界失控Hacker News上那场关于Supabase MCP组件导致数据库直连公网的讨论,根源不在代码里埋了后门,而在于两处可避免的失控:默认配置放行了数据库连接池:MCP服务启动时未强制隔离数据库访问通道,DATABASE_URL 环境变量被直接注入到客户端可触达的连接上下文中;请...

MCP协议安全:避免Supabase式数据库暴露的实战要点
Supabase事件复盘:不是“漏洞”,是配置与边界失控
Hacker News上那场关于Supabase MCP组件导致数据库直连公网的讨论,根源不在代码里埋了后门,而在于两处可避免的失控:
- 默认配置放行了数据库连接池:MCP服务启动时未强制隔离数据库访问通道,
DATABASE_URL环境变量被直接注入到客户端可触达的连接上下文中; - 请求解析层缺失数据范围约束:MCP协议允许客户端传入原始SQL片段或表名参数,但服务端未校验这些输入是否落在预设白名单内,也未绑定用户身份与数据租户(tenant)。
结果是:攻击者用 curl -X POST https://your-app.supabase.co/mcp/query -d '{"table":"users","where":"1=1"}' 就能拉走全量用户记录。
这不是MCP协议本身的设计缺陷,而是实现时跳过了权限锚点和数据沙箱。
关键防御点:从协议层落地到代码
1. 权限控制必须绑定租户上下文
JWT认证只是起点。真正的权限控制发生在每次MCP请求进入时——必须将用户身份、角色、租户ID三者绑定,并在数据库查询前完成校验。
不要只验证token是否有效,要验证token声明中tenant_id是否匹配当前请求目标资源所属租户。
// ✅ 正确:在MCP handler中做租户级拦截
app.post('/mcp/query', authenticateJWT, (req, res) => {
const { table, where } = req.body;
// 检查该用户是否有权访问此表(基于租户+角色策略)
if (!isTableAccessible(req.user.tenant_id, req.user.role, table)) {
return res.status(403).json({ error: 'Forbidden: table access denied' });
}
// 构造查询时强制注入租户过滤条件
const safeWhere = { ...where, tenant_id: req.user.tenant_id };
db.query(table, safeWhere).then(data => res.json(data));
});2. 数据边界校验不能依赖客户端输入
MCP协议不禁止客户端传表名或字段名,但服务端必须用白名单机制兜底:
- 表名只允许出现在预定义列表中(如
['posts', 'comments', 'profiles']); - 字段名需映射到实体属性,禁止原始SQL拼接;
where条件必须通过结构化解析器(如objection.js的QueryBuilder或knex().where())生成,禁用字符串模板。
// ❌ 危险:拼接SQL
const query = `SELECT * FROM ${req.body.table} WHERE ${req.body.where}`;
// ✅ 安全:白名单 + 结构化构建
const allowedTables = new Set(['posts', 'comments']);
if (!allowedTables.has(req.body.table)) {
throw new Error('Invalid table name');
}
// 使用Knex构建带租户约束的查询
const result = await knex(req.body.table)
.where({ tenant_id: req.user.tenant_id })
.andWhere(req.body.where || {});3. 加密不是选项,是通信基线
MCP Server必须强制HTTPS,且所有Agent通信链路默认启用TLS 1.3。别在开发环境留HTTP后门——.env里写NODE_ENV=development不等于可以关掉证书校验。
对敏感字段(如API key、token、PII),额外做应用层加密:
// 使用AES-GCM加密存储敏感字段(非仅哈希)
const crypto = require('crypto');
const algorithm = 'aes-256-gcm';
const secretKey = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(text) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}:${cipher.getAuthTag().toString('hex')}`;
}实战:Yitb Server鉴权中间件与Agent设计原则
鉴权中间件要覆盖MCP入口点
MCP请求常走独立路由(如 /mcp/ 前缀),不能复用Web页面的鉴权逻辑。中间件必须:
- 解析
Authorization头中的Bearer token; - 校验签名、过期时间、租户声明;
- 将
user.tenant_id注入req,供后续handler使用。
// middleware/mcp-auth.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
if (!payload.tenant_id) {
throw new Error('Missing tenant_id in token');
}
req.user = payload;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid or expired token' });
}
};在路由中显式挂载:
const mcpAuth = require('./middleware/mcp-auth');
// ✅ 专用于MCP协议的入口
app.post('/mcp/query', mcpAuth, handleMCPQuery);
app.post('/mcp/insert', mcpAuth, handleMCPInsert);Agent安全设计三条铁律
- 永远不持有长期数据库凭证
Agent运行时只获取短期、作用域受限的访问令牌(如PostgreSQL的pgbouncer动态用户,或Supabase的service_role临时token),用完即焚。 - 所有出站请求强制双向TLS
Agent调用外部API时,不仅验证服务端证书,还要用自己的客户端证书发起请求(mTLS),防止中间人伪造响应。 - 日志脱敏是硬性要求
记录MCP请求时,自动过滤password、api_key、token等字段,且不记录原始SQL——只记操作类型、表名、影响行数、耗时。
// 日志中间件示例
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const safeBody = { ...req.body };
delete safeBody.password;
delete safeBody.api_key;
console.log({
method: req.method,
path: req.path,
status: res.statusCode,
duration,
body: safeBody,
timestamp: new Date().toISOString()
});
});
next();
});