supabase/mcp 发现 composite foreign key 处理错误,多列外键错展为笛卡尔积。揭示 AI 系统依赖数据源准确性的关键安全风险,agent 无法察觉。
几周前,我在 supabase/mcp 中遇到了这个问题。它现在已经合并(PR #317),但这种 bug 的形态值得记录下来,因为这恰恰是 AI Agent 会毫不怀疑地直接消费的那类错误。
背景是这样的:MCP server 提供了一个 list_tables 工具。详细模式会包含外键关系,因此当 Agent 询问“这些表之间是如何关联的”时,它会把工具输出当作事实依据。
但对于复合外键,这个输出是错误的。SQL 在连接源列和目标列时,没有按照位置逐一配对,因此一个包含 2 列的外键会返回 4 组配对,包含 3 列的外键则会返回 9 组。笛卡尔积。N 列会产生 N 的平方个关系,而其中除 N 个以外的关系,在 schema 中根本不存在。整个过程不会报错,输出看起来也完全合理。人类或许会盯着它琢磨一下,但 Agent 只会直接使用它,于是开始基于一个从未存在过的关系执行连接。
我最初以问题而非定论的形式提交了 issue,因为我觉得自己可能遗漏了某些上下文:这是有意设计的行为吗?维护者确认这是一个 bug,并建议既然已经在处理,不妨再进一步:把每个约束中的列组织成有序数组,让配对关系由数据结构明确表达,而不是通过相邻行来暗示。
我真正想谈的是,如何证明这个修复是正确的。
显而易见的修复方案是“对列进行排序”。但问题是,按什么排序?按字母顺序,在理想路径下可行;按列在表中的物理顺序,在理想路径下同样可行。但这两种方式都是错的。配对必须遵循约束自身声明的列位置,因此修复使用了 unnest ... WITH ORDINALITY。我还添加了一个测试,其中外键的声明顺序既不同于字母顺序,也不同于物理顺序。这个测试才是不变量。没有它,这个 bug 完全可能在测试套件一片绿色的情况下卷土重来。
分组后的数据结构提交之后,我希望在合并前,除了自己的测试之外,再对它做一次合理性检查。于是,我在这个分支上运行了一次自动化审查:LLM 根据修改意图和 diff 提出边界场景测试,然后由一个确定性的 gate 在真实代码上逐一执行。整个通过或失败的判定过程中,没有任何模型参与。它在我已经编写的测试之外,又提出了 9 个场景:同一对表之间存在两个相互独立的复合外键时,二者仍然保持分离;自引用复合外键;从关系任意一侧都能看到的跨 schema 外键;单列外键能正确输出为只含一个元素的数组;重复调用会返回完全相同的输出。9 个场景全部执行通过,没有发现任何缺口。我把这份列表发到了 PR 中,并表示可以把它们提交进去。
维护者的回复是:全部提交,然后就合并。于是这些测试都加了进去,外加审查过程中出现的另外几个场景,最终测试套件以 110/110 全部通过的结果落地。
我从中得到两点体会。
第一,面向 Agent 的 API 中,真正危险的 bug 并不是崩溃,而是那些信心十足地返回错误数据的 bug。崩溃会被第一个重试循环捕获,但 N 的平方个外键关系却会被继续用于构建系统。
第二,“我的测试通过了”和“这个行为已经被测试牢牢锁定”是两个不同的判断。那 9 个实际执行的场景没有发现缺口,这没问题,因为它们的作用正是如此:把“我认为这个修复是正确的”,转化为“这些场景已经针对它实际执行过,并且全部成立”。最终被合并的是证据,而不是我的信心。
我使用的这个 gate,是我一直在公开开发的工具。它叫 edgeverdict,已经发布在 PyPI 上,并提供了一个无需 key、约半分钟即可运行完毕的 demo。如果你把它用在自己的 repo 上,请告诉我它会在哪里欺骗你。这确实是你能为它做的最有价值的事情。
工具链接见第一条评论。
部分评论可能仅对已登录的访问者可见。登录后即可查看全部评论。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。