MCP协议配置不当致数据库裸奔:SQL注入风险与安全配置指南

MCP竟成数据库裸奔开关?——从Hacker News热议案例看MCP协议配置不当的致命风险
直击痛点:你的AI项目是否正暴露在SQL注入的威胁之下?
Hacker News上一则帖子火了:一家AI创业公司用Supabase做后端,MCP Server配错了,整个数据库的SQL查询逻辑直接裸奔。攻击者拿到一个普通用户token,就能查遍所有表、拖走全部数据。
这不是理论风险。我们翻过几份真实MCP Server代码,发现类似配置错误高频出现:权限开得过大、元数据接口没关、输入校验形同虚设。
如果你正在用MCP协议连数据库,别跳过这一节。
案例回顾:Supabase中的MCP协议漏洞如何导致全库SQL泄露?
这家公司做了三件事,把数据库大门钥匙焊死在门把手上:
1. Server端权限设为*
MCP Server配置里写了"permissions": ["*"]。任何通过身份验证的用户(哪怕只是登录态的前端用户)都能执行任意SQL。Supabase的Row Level Security(RLS)策略完全失效——因为MCP Server绕过了RLS,直连PostgreSQL。
2. 元数据接口开着公网入口
/v1/metadata/tables 和 /v1/metadata/columns 这两个端点没加鉴权,也没走内网。攻击者curl一下,立刻拿到所有表名、字段类型、主键关系。下一步就是拼UNION SELECT语句。
3. 访问控制粒度停留在“有无token”层面
没有角色区分,没有表级白名单,没有字段掩码。一个客服账号和DBA账号在MCP Server眼里毫无区别。
结果:攻击者用' OR 1=1 --触发报错,看到完整SQL模板;再构造SELECT * FROM users,拿到明文邮箱和密码哈希;最后用COPY users TO PROGRAM 'curl -X POST ...'外传全量数据。
根本原因解析:MCP协议配置不当的致命缺陷
MCP本身不背锅。问题出在开发者把MCP Server当成了“带认证的psql客户端”,忽略了它本质是数据库代理层。
1. 权限管理被当成可选项
MCP规范明确要求Server实现permissions字段校验,但很多实现直接忽略或硬编码为["*"]。更危险的是,部分SDK自动生成的配置模板就默认开全权限。
2. 元数据接口设计即暴露面
/metadata不是调试后门,它是生产环境的高危端点。Supabase官方文档强调:“禁用元数据端点是生产部署的强制步骤”,但没人读。
3. 访问控制逻辑与数据库脱钩
MCP Server应该继承数据库的权限模型(比如PostgreSQL的role+schema+table三级权限),而不是另起一套粗粒度RBAC。常见错误是只校验token有效性,不校验该token对应的角色能否访问目标表。
4. 日志缺失导致攻击静默进行
所有SQL执行日志只写到stdout,没落盘、没告警、没采样。攻击者执行500次SELECT,运维系统零告警。
关键防御点:如何构建安全的MCP Server?
1. 最小权限原则必须落地
删除所有
["*"]配置,显式声明每个角色的权限:{ "role": "analyst", "permissions": [ {"table": "orders", "actions": ["SELECT"]}, {"table": "products", "actions": ["SELECT"]} ] }- 禁用
INSERT/UPDATE/DELETE权限,除非业务强依赖。读多写少场景下,90%的MCP调用只需SELECT。 - 定期用脚本扫描配置文件,自动告警未声明权限的表。
2. 访问控制必须绑定数据库原生能力
不要自己实现表级ACL。让MCP Server调用PostgreSQL的
has_table_privilege()函数校验:SELECT has_table_privilege('analyst_role', 'orders', 'SELECT');对敏感字段(如
users.email,payments.card_number)启用动态列掩码,在SQL执行前重写查询:-- 原始请求 SELECT id, email FROM users WHERE id = 123; -- 实际执行(对非admin角色) SELECT id, '***@***.com' AS email FROM users WHERE id = 123;
3. 输入校验不是防SQL注入的主力,而是最后一道保险
参数化查询必须强制启用。禁止任何字符串拼接SQL:
# ❌ 危险 cursor.execute(f"SELECT * FROM {table} WHERE id = {user_input}") # ✅ 正确(使用pg8000或asyncpg的参数占位) cursor.execute("SELECT * FROM users WHERE id = $1", [user_id])对表名、字段名等标识符,用白名单校验:
ALLOWED_TABLES = {"users", "orders", "products"} if table_name not in ALLOWED_TABLES: raise PermissionError("Table not allowed")
4. 元数据接口必须物理隔离
- 生产环境彻底删除
/metadata路由。调试需求用本地supabase db remote schema pull替代。 如果必须保留,加两道锁:
- 只允许
127.0.0.1和CI/CD服务器IP访问 - 强制HTTP Basic Auth,凭证单独保管,不进Git
- 只允许
5. 日志必须包含可追溯的上下文
每条SQL执行日志至少记录:
- 请求ID(用于链路追踪)
- 用户角色(不是用户名,是实际生效的DB role)
- 表名 + 字段数 + 扫描行数(
EXPLAIN (FORMAT JSON)第一层) - 执行耗时(毫秒级)
示例日志:
[req-abc123] analyst@prod SELECT orders.id,orders.status FROM orders WHERE created_at > '2024-01-01' | rows=1240 | time=84ms6. 安全审计不能靠自觉
每次MCP Server发布前,运行
mcp-audit工具(开源,见GitHub yitb/mcp-audit):- 检查配置中是否存在
["*"] - 扫描代码中是否有
cursor.execute(字符串拼接 - 验证
/metadata路由是否注册
- 检查配置中是否存在
- 每季度请第三方做渗透测试,重点打
/query端点的SQL注入和权限越界。
实战案例:如何应用上述原则构建安全的MCP Server?
下面是一个精简但生产可用的MCP Server核心逻辑(基于FastAPI):
from fastapi import FastAPI, HTTPException, Depends, Header
from sqlalchemy import text
from typing import List, Dict, Any
app = FastAPI()
# 从配置加载权限白名单(JSON文件或DB)
PERMISSIONS = {
"analyst": [{"table": "orders", "actions": ["SELECT"]}],
"support": [{"table": "users", "actions": ["SELECT"]}]
}
def get_user_role(x_role: str = Header(..., alias="X-Role")) -> str:
if x_role not in PERMISSIONS:
raise HTTPException(403, "Invalid role")
return x_role
@app.post("/query")
def execute_query(
query: Dict[str, Any],
role: str = Depends(get_user_role)
):
table = query.get("table")
action = query.get("action", "SELECT").upper()
# 1. 白名单校验
allowed = False
for perm in PERMISSIONS[role]:
if perm["table"] == table and action in perm["actions"]:
allowed = True
break
if not allowed:
raise HTTPException(403, f"Role {role} cannot {action} table {table}")
# 2. 字段掩码(示例:隐藏email)
if table == "users" and "email" in query.get("columns", []):
query["columns"].remove("email")
query["columns"].append("REPLACE(email, '@', '*') AS email")
# 3. 参数化执行(假设用asyncpg)
sql = f"SELECT {', '.join(query['columns'])} FROM {table}"
if "where" in query:
sql += f" WHERE {query['where']}"
# 实际执行时用 $1, $2 占位符,此处省略参数绑定细节
return {"result": run_sql(sql, query.get("params", []))}关键点:
- 权限检查在SQL生成前完成,不依赖数据库返回错误
- 字段掩码在SQL拼装阶段介入,避免敏感字段进入查询
X-Role头由上游网关(如Traefik)根据JWT claim注入,不信任客户端传值
下一步行动:如何夯实你的MCP Server安全能力?
- 立刻检查你的MCP Server配置:搜
"permissions": \["\*\]"、/metadata路由、cursor.execute(字符串拼接 - 禁用元数据接口:删掉相关路由,或加IP+Basic Auth双锁
- 部署日志采样:用Loki+Grafana监控
/query端点的rows_scanned > 1000事件 - 参考加固清单:yitb.com/mcp-hardening-checklist(开源,含Terraform检测脚本)
安全不是功能列表里的最后一项。它是MCP Server启动时第一个必须通过的健康检查。