文章通过 Cursor 先生成 SQL 注入漏洞、随后又能识别该漏洞的案例,说明代码生成与安全评估可能表现不一致。作者建议将独立安全检查纳入流程,而不能只依赖安全提示词或同一模型自检。
AI 编辑器并不是从安全文档中学习安全知识的。它们学自教程,而教程为了保持易读性,往往会刻意省略安全控制措施。
模型并非不知道这些知识。把它自己生成的内容粘贴回去,它就能正确指出其中的漏洞。生成与评估走的是两条不同的路径。
在 prompt 中强调安全确实有帮助,但无法覆盖模型的先验倾向。你需要用不受相同先验影响的工具检查输出。
上周,我让 Cursor 给一个小型 Express API 添加搜索 endpoint。它给了我三行代码,直接把查询参数插进了 SQL 字符串。
我把完全相同的代码块粘贴回同一个对话,问它是否安全。它告诉我,这段查询存在 SQL 注入漏洞,解释了漏洞原理,并给出了参数化查询的改写版本。
同一个模型。同一个会话。前后只差九十秒。
相比漏洞本身,这种落差更让我不安。如果一个模型无法识别 SQL 注入,那是训练问题,可以通过增加数据来解决。但一个模型明明能够准确识别漏洞,却依然会写出这样的代码,那就完全是另一回事了。
AI 编辑器学习编程的主要来源,是教程、快速入门指南和 Stack Overflow 回答,而不是生产级代码库或安全文档。仅这一事实,就决定了它们生成一切代码时的默认倾向。
想想看,究竟哪些代码会公开出现,并被大规模复制。一篇热门的“用 Node 构建 REST API”教程,可能会被复制进数千个 repo,被十几个博客转载,还会在数百条 Stack Overflow 回答中被引用。真正通过安全审查的生产代码通常位于私有 repo 中;即使偶尔公开,也通常只存在一份。
训练语料库并不是代码的随机样本。它严重偏向为教学而编写的代码。
教程会删掉所有不会改变读者屏幕所见结果的内容,而所有安全控制在正常运行时都是不可见的。这并非粗心大意,而是让教程保持易读的编辑原则。
逐项看看就明白了。对于正常输入,参数化查询和字符串插值查询会产生完全相同的输出。只有当有人滥用 endpoint 时,rate limiter 才会产生作用。对于本来就拥有该记录的用户,所有权检查会悄无声息地通过。恒定时间比较和普通相等比较返回的 boolean 结果也完全一样。
这些措施无一例外都会增加代码行数,却不会改变读者能够观察到的结果。如果你的编辑原则是“删掉所有不能推动演示继续进行的内容”,那么每一次,最先被删掉的都会是安全控制,而且整个过程甚至不需要做出任何与安全有关的决定。
语料库中的这种偏差并不是随机噪声,而是始终指向同一个方向。
教程作者通常确实会指出这些缺失,但他们是在代码块旁边的文字中提醒读者,而真正得到传播的是代码块。
教程中到处都是这样的句子:“为简单起见,这里跳过验证”,或者“请勿在生产环境中这样做”。这些句子位于代码片段上方或下方的段落中。被复制进 repo 的是代码片段。被下一篇博客文章引用的也是代码片段。即便提醒被写成行内注释,例如 // in production, load this from an environment variable,第二个粘贴代码的人通常也会将其删除,因为代码一旦进入真实文件,这种注释看起来就像多余的杂物。
于是,这种代码模式以完整的强度不断传播,而相关警告却在每一次转手时持续衰减。等到所有内容最终进入训练数据时,代码已经随处可见,警告却十分罕见。
模型之所以能够正确解释漏洞,是因为解释过程会调用语料库中的安全类文章。代码生成则依赖代码本身,而代码的分布由教程塑造。这是两种不同的数据分布,而且它们彼此冲突。
下面是一个可复现的例子。让模型生成一个搜索 endpoint:
// Prompt: "add a product search endpoint"
app.get('/api/search', async (req, res) => {
const q = req.query.q;
const rows = await db.query(
`SELECT * FROM products WHERE name LIKE '%${q}%'` // CWE-89
);
res.json(rows);
});
把这段代码粘贴回去,再问一句“这安全吗?”,你就会得到关于 CWE-89 的准确回答,以及修复后的代码:
app.get('/api/search', async (req, res) => {
const rows = await db.query(
'SELECT * FROM products WHERE name LIKE $1',
[`%${req.query.q}%`]
);
res.json(rows);
});
这些知识从一开始就存在。只是第一次生成代码时,没有任何因素要求模型调用它们。
这改变了我们看待问题的方式。你不是在填补知识空白,而是在与一种先验倾向竞争。
要求模型编写安全代码,可以改变最显眼的代码模式,却无法覆盖所有细节。随着会话变长,模型的先验倾向还会重新占据上风。
这种情况我见得太多,已经不再信任单纯的 prompt 约束。要求模型编写“一个安全的登录 endpoint”,你几乎总能得到 bcrypt,因为在整个语料库中,“安全登录”与 bcrypt 之间存在很强的关联。但你不一定会得到 rate limiter,因为“安全”这个词还不够具体,无法把它召唤出来。这条指令改变了它明确指向的部分,其余部分仍然由默认模式填充。
当一个会话已经处理到第二十个文件时,那条指令早已退居 context 深处,而局部代码模式产生的压力就在眼前。默认倾向于是卷土重来。这不是因为模型忘记了,而是因为当前生成过程中,没有任何因素要求它写出不同于典型代码的东西。
你需要使用不受模型相同先验影响的工具检查输出。在实践中,这意味着使用 scanner 或 hook,也可以再增加一次专门负责 review 的独立检查。
这里真正有用的是这种不对称性。模型擅长评估代码,却会在生成代码时受到偏差影响,因此让它进入评估模式并不是浪费。对生成代码单独进行一次 review,确实可以发现相当一部分由生成过程引入的问题,恰恰因为这次 review 走的是那条“知道相关知识”的路径。
然而,由同一个模型执行的 review 仍会继承相同的盲区,确定性工具则不会。semgrep 对搜索 endpoint 通常应该长什么样没有任何主观意见。它只有规则,而且无论检查的是第一个文件还是第一千个文件,规则都会以相同的方式触发。
这就是全部诀窍。既然偏差是系统性的,检查也必须是系统性的。
问:更新的 AI 模型会编写更安全的代码吗?
**答:**会好一些,但底层语料库并没有改变。新教程仍然以同样的方式编写,也以同样的方式被抓取,而且越来越多的新教程本身就是 AI 根据旧有数据分布生成的。更好的 alignment 可以减轻这种影响,却无法消除其根源。
问:我能不能只告诉 Cursor,让它编写安全代码?
**答:**这确实有帮助,而且你也应该这样做,但不要把它当作完整的安全保障。指令可以可靠地影响你明确指出的特定模式,但周围的空白仍会由默认倾向填补。请使用 scanner 验证输出。
问:为什么模型能发现漏洞,却无法避免写出漏洞?
**答:**解释漏洞时,模型会调用训练数据中的安全类文章。生成代码时,模型依赖的是代码数据分布,而其中占主导地位的是为了教学而刻意省略安全控制的代码。这两部分知识模型都正确地学会了,只是它们彼此冲突。
我一直在使用 SafeWeave 处理这个问题。它以 MCP server 的形式接入 Cursor 和 Claude Code,并在我继续下一步之前检查生成的代码;检查依据是规则,而不是模型对自身输出的判断。即便只是一个使用 semgrep 和 gitleaks 的基础 pre-commit hook,也能捕获教程式默认倾向产生的大多数问题。无论使用什么工具,关键都是要用一个不受同样偏差影响的东西进行验证。
完整原文可在 SafeWeave 博客阅读:https://safeweave.dev/blog/cursor-learned-to-code-from-tutorials-that-skip-security
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。