国产日韩欧美-97在线观看免费-亚洲欧洲日韩-亚洲free性xxxx护士白浆-国产精品网站在线观看-日韩有码一区-日本大片在线观看-欧美精品一区二区性色a+v-97在线精品-亚洲久草视频-天天操人人爽-你懂的国产精品-国产国语对白-就去干成人网-亚洲美女色视频

?? 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();
});
返回首頁