分析 AI Agent 执行数据库写入的五个拦截层级,从模型提示词到数据库角色权限,并给出每层失效后的实际风险。
2025 年 7 月,Replit 的 Agent 在一次明确的代码和操作冻结期间,删除了一个在线生产数据库。Replit 的 CEO 称这是不可接受的,随即上线了开发/生产环境分离和更好的回滚机制。
冻结是真实存在的。它被写下来了,被同意了,Agent 也被告知了——但这些都没有出现在执行路径中。那条指令存在于模型的上下文中,而模型的上下文不是一个强制执行规则的地方。
还有第二个值得深思的细节。之后,Agent 报告说回滚是不可能的。这是错误的,而且延误了恢复。所以系统自己对所发生事情的说法不仅仅是毫无帮助,而是在关键的点上自信地错了。
这个事件是这篇文章要讨论的问题的最简版本。这个领域中的每个工具都有护栏。它们在护栏处于哪一层上有巨大差异,而且几乎没有一个会告诉你。
我曾试图找一个按这种方式组织的对比,但没有找到,所以这是我的版本。我自己在这个领域也有一个工具;它在最后,它之前的替代方案有名字,其余的我都尽量按维护者的描述方式来描述。
这篇文章的早期版本经过了几位专门审查的人的审核,看我是否有美化自己的工具或错误描述他人工具的地方。他们发现了几处。修正已写入正文,而不是放在脚注里。
关于数据库写入的规则可以在五个地方执行。它们并不同等优秀,差别不是程度上的。
这个阶梯的有用特性是:每一级都活在上一级失败之后。一个没有写权限的数据库角色不会在乎你的白名单是否有解析 bug、用户是否点击了"总是允许"、或者模型是否被一张它读到过的工单说服了什么事。
这个领域的大部分争论实际上是:工具位于不同的阶梯上,却使用着相同的词汇。
这是工具描述、系统提示词,以及在工具结果中返回的指令——"仅用于读取查询"、"应用前先询问用户"。
它没有安全价值。值得写,因为它改善了普通行为,而普通行为就是大多数行为。它不是一道控制,2025 年 7 月的事件就是有人相信它是的之后看起来的样子。
有一种特定版本值得命名,因为它容易被误认为是 Layer 3。一些 MCP 服务器实现了两步写入流程——准备,然后完成——步骤之间的区分是工具描述告诉模型在此之间进行验证。如果第一次调用从未真正被检查过,第二次调用也没有任何拒绝,那就是 Layer 1 穿了 Layer 3 的衣服。
每个严肃的 MCP 客户端都会在工具调用前显示确认对话框,Agent 框架也有自己的等价物:LangChain 的 HumanInTheLoopMiddleware 和 interrupt,OpenAI Agents SDK 的 needsApproval 配合可恢复中断,Pydantic AI 的 requires_approval 延迟工具。这些是真实存在的、构建良好的机制,如果你正在构建一个向任何东西写入的 Agent,你应该使用它们。
人类通常看到的是模型自己的工具输入。对于数据库写入,这意味着一个 SQL 字符串,渲染为 JSON。不是它将触及的行,不是多少行,不是它们当前包含什么。同样的客户端把文件编辑渲染为彩色 diff,却把数据库变更渲染为一团文本。你被要求批准一个声明,这意味着你被要求成为一个 SQL 解释器,在脑子里,面对你看不到的数据。
而且这些门会被关掉。--dangerously-skip-permissions、自动运行模式、白名单蔓延。这不是用户错误;这是反复要求一个人每小时回答二十次"他是否那个意思"的可预期结果。任何成本是一次点击的控制,反复支付,最终会收敛到被禁用。
一种规避两个问题、且因无聊而未被充分认可的设计:让 Agent 生成一个 artifact,进入你已有的审查流程。PR 中的一个迁移文件。一个预演表里的一行,由一个可信的 job 来应用。然后人类在专为审查而构建的工具中审查——有历史记录、有 blame、有第二双眼睛——而不是在一个打断他们的模态框里。
这是大多数数据库 MCP 服务器放置护栏的地方,也是有趣失败发生的地方,因为这里的服务器持有一个可以写入的凭证,只是在选择不使用它。
至少有四种不同的机制,它们并不同等强大。
在只读事务中包装查询。原始的 Postgres MCP 参考服务器就是这么做的:BEGIN TRANSACTION READ ONLY,运行 SQL,在 finally 中回滚。Datadog Security Labs 展示了它可以通过语句堆叠被绕过:Postgres 驱动在一个调用中接受多个分号分隔的语句,所以 COMMIT; DROP SCHEMA public CASCADE; 结束了只读事务,之后的一切都以完整权限运行。
他们推荐的修复方案是整篇文章的论点,来自一个刚刚完成破解应用层版本的安全团队:
一种可能的缓解措施——我们无论如何都建议这样做——是使用权限受限的 Postgres 用户。你绝对应该这样做。
他们的结论是朴实无华的,值得重述:"经典的应用程序安全漏洞与 MCP 服务器及其他 AI 工具仍然高度相关。"
这个教训可以用我的话泛化:只读事务不是安全边界,当协议接受分号时。
那个服务器在 2025 年 7 月被废弃并归档了。它在 2026 年 8 月 2 日那一周仍有 86,941 次 npm 下载,我写这篇文章时查的。
关键词阻断。AWS 的数据库 MCP 服务器默认为只读,拒绝 INSERT、UPDATE、DROP、会话状态语句以及一串危险函数。他们自己的 README 对此能给你什么异常直白:
将这视为尽力而为的、深度防御机制,而不是安全边界。黑名单无法列举每一种危险构造,而足够有创意的一个查询(混淆、引号标识符、新服务器/扩展函数等)可能绕过它。不要依赖它作为你唯一的控制。
然后说了这篇文章整篇在说的事情,比我说得更好:
将最小权限角色(数据库强制执行)与黑名单(应用层强制执行)相结合,给你深度防御。
数据库强制执行和应用层强制执行。这是厂商自己文档中的区别,而这是几乎没有其他任何东西明确说出来的。
正确地解析 SQL。优于关键词匹配,但它移动了问题而非消除了问题:你现在是在维护一个 SQL 解析器,它必须永远与数据库的解析器在每个方言怪癖上保持一致。两个解析器之间的分歧就是整个 bug 类。
根本不注册写工具。Neon 的只读模式限制了存在哪些工具。这一点干净利落、易于推理——但它仍然是 Layer 3:进程中的凭证可以写入,任何到达它的东西也可以。
一个值得命名的陷阱,因为我差点把它归到 Layer 5:Postgres 的 default_transaction_read_only 看起来像引擎级强制执行,但实际上不是。它是一个 USERSET 参数——一个默认值,不是权限——所以 SET default_transaction_read_only = off; 或 BEGIN READ WRITE; 可以从它本应约束的会话内部恢复写入。打败只读事务的同一个分号打败了这个。它只有在角色没有写权限时才起支撑作用,在这种情况下,权限才是控制,而那个设置只是装饰。
还有一类服务器故意把事务控制交给模型——将 begin transaction、commit、rollback 作为可调用工具——或者一个接受多语句的通用 execute_sql。mcp-node-mssql 和 mcp-sqlite-tools 是我验证过的例子。对于可信的本地开发数据库,这是一个合法的设计。值得对任何其他场景下的含义保持清醒:模型握着提交按钮。
选择不是被迫的,值得提名一个走另一条路的服务器。mssql-mcp-node 默认只读,将写操作拦在 MSSQL_ENABLE_WRITES 环境变量之后,并在它总是回滚的事务中运行每个读取。同样的协议,同样的语言,相反的默认值。
位于客户端和服务器之间的 MCP 网关可以集中拦截工具调用,在一个地方记录它们,并从 OPA 或 Cedar 这样的引擎应用策略。这是"我们四十个 Agent 中的哪些可以访问哪些服务器"的正确答案,并能在一个被入侵的客户端中存活。
它的限制是结构性的:它保护通过它的路径。如果凭证也可以直接到达,代理只是一个约定。
这是唯一强制执行能超越其上所有层存续的阶梯。
没有写权限的角色。Supabase 的 MCP 只读模式以一个只读 Postgres 用户运行查询——在角色层面强制执行,而不是在进程中。它是 opt-in 而不是默认,这是一个真实的批评,但机制是强大的那个。
引擎级只读。ClickHouse 的 readonly=1(也禁止更改设置),以及 SQLite 的只读连接。
作用域写权限,这在 Agent 讨论中几乎没有出现,却是实际问题的成熟答案。行级安全、列级授权、可更新视图、SECURITY DEFINER 函数作为唯一写路径。"可以写入,但只能写这些行"是一个有十年生产使用经验的已解决问题,它完全可以映射到 Agent 上。
完全不用原始 SQL。类型化的参数化端点——PostgREST、Hasura、存储过程、一个暴露动词而非一个 SQL 框的 MCP 服务器。Agent 选择一个操作并填充类型化槽位;SQL 是由人类预先编写的。这是生产系统中最常见的答案,却在 Agent 写作中讨论得最少。
问题是 Layer 5 控制的是什么事可以发生,而不是什么事将要发生。UPDATE orders SET status='shipped' WHERE placed_on < '2026-08-01' 被任何合理的权限模型允许。它触及四行还是四万行不是一个权限问题。
只读不等于安全。移除写操作不会移除风险:无界的读取是一种拒绝服务攻击和成本事故,而读取访问是每个严重 prompt 注入场景中数据泄露的那一半。有几起记录最差的 MCP 事件根本不涉及任何写操作。
审批不能是完整答案,因为越来越多的 Agent 运行没有人类在场——定时任务、CI、批处理运行。对于那些,唯一可行的组合是 Layer 5 加上上限加上可逆性加上审计。
可逆性值得与预防获得同等的重视,但几乎得不到任何。快照恢复、时间穿越、即时分支恢复、时间表、软删除。在实践中,这是大多数团队真正依赖的,而重要的问题——能回溯多深、是否覆盖 DDL、Agent 本身能否调用恢复、以及你是否会在需要时注意到你需要它——很少在需要之前被问到。
在审批之前测量不是新类别
我也应该在这里命名替代方案,因为这是阶梯上我卖东西的唯一一部分。
Bytebase 的 SQL review 计算受影响的行和风险级别,并在 rollout 前将变更路由给人类。基于分支的数据库——Neon、PlanetScale——让你应用到一个副本并检查结果后再提升,从不同方向回答了同样的问题,并给你一个免费的回滚故事。而最便宜的版本根本不需要工具:让 Agent 生成匹配的 SELECT,或在带有 RETURNING 的开放事务中运行语句,在提交前读回。
我维护着 llm-safe-sql,它专门解决 Layer 2 的问题:人类正在批准一个声明。它在事务中运行提议的 UPDATE 或 DELETE,测量真实的之前/之后值,总是被回滚,并向人类展示那个测量。应用发生在单独的进程中,从单独的凭证。
在这个阶梯上要诚实,而且比我第一稿更诚实。测量运行在 Layer 3。规划的凭证和应用的凭证都可以写入——一次 dry run 不能在没有写权限的情况下执行——所以数据库没有在它们之间强制执行分离。库和进程边界才是。把单独的应用凭证称为"Layer 5",就像我最初写的那样,是在我的阶梯上把我的工具提升了一级。
它真正具有 Layer 5 设置的是为模型的读取使用一个单独的只读角色,这也是运营商在这里能做的最便宜的事情。而它的 check 命令现在打印每个护栏实际位于哪里,并命名那些是虚构的,因为"应用使用与规划相同的凭证"是运营商应该被告知的,而不是要自己去发现的。
还有诚实的限制:这是人类在循环中,所以无人值守的 Agent 超出范围。它不解决 prompt 注入——一个测量到的 diff 告诉你会发生什么,而不是谁选择的,尽管"会发生什么"比模型自己描述的要多得多。在人类决策之前拦截数据库访问不是新东西——像 StrongDM 这样的特权访问代理在有语言模型指向生产数据库之前的很久以前,就已经在会话中途根据策略检查命令了。
如果你在评估这个领域的任何东西,区分选项的问题不是它有哪些功能。是:
当你的护栏拒绝某事时,是什么在拒绝——而且如果那个组件有 bug,什么仍然会拒绝它?
如果答案是"工具描述",你有一个约定。如果是"审批对话框",你有一个约定加一个习惯。如果是"一个不能写入的角色",你有一个控制。
大多数工具是混合的,这没问题。不知道哪个是哪个就不是。
以上所有内容在写作时都对照一手资料进行了核实。如果一个声称来自厂商的比较页面说竞争对手的事,我就删掉了。
MCP vulnerability case study: SQL injection in the Postgres MCP server — Datadog Security Labs
Replit AI wiped a database and called it a catastrophic failure — Fortune, July 2025
awslabs/mcp — postgres-mcp-server
Supabase MCP server docs
neondatabase/mcp-server-neon
ClickHouse MCP server
crystaldba/postgres-mcp
bytebase/dbhub and Bytebase SQL review
mihai-dulgheru/mssql-mcp-node — read-only by default
PostgreSQL: default_transaction_read_only
LangChain human-in-the-loop middleware
OpenAI Agents SDK — guardrails and human review
Pydantic AI — deferred tools and approval
Download figure for @modelcontextprotocol/server-postgres taken from the npm registry API for 2026-08-02 to 2026-08-08.