🧩 MCP生态

MCP协议安全配置指南:防范Supabase式数据库暴露的实战要点

发布时间:2026-04-17 分类: MCP生态
摘要: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.jsQueryBuilderknex().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安全设计三条铁律

  1. 永远不持有长期数据库凭证
    Agent运行时只获取短期、作用域受限的访问令牌(如PostgreSQL的pgbouncer动态用户,或Supabase的service_role临时token),用完即焚。
  2. 所有出站请求强制双向TLS
    Agent调用外部API时,不仅验证服务端证书,还要用自己的客户端证书发起请求(mTLS),防止中间人伪造响应。
  3. 日志脱敏是硬性要求
    记录MCP请求时,自动过滤passwordapi_keytoken等字段,且不记录原始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();
});
返回首页