研究者用 26 条确定性规则(非 AI)重新扫描 Lovable/Bolt/v0 生成的代码,发现约 12.5% 的仓库存在关键缺陷,且首次 AI 扫描存在不可复现性问题。
去年九月,我发表了一篇文章,讲述了一个语言模型在 400 个 AI 构建的代码仓库中发现了什么。最有用的批评,同时也是最显而易见的批评是:模型的判断是不可复现的,你无法审计它。再跑一次,你可能会得到不同的答案。问"你到底是怎么统计出来的?",诚实的回答是"模型决定的。"
所以我用确定性规则重新跑了一遍。同样的冻结语料,不使用模型,26 条规则,每条发现都可以追溯到某条规则 ID 和行号。
标题数字下降了。这成了这次实验最有趣的部分。
400 个使用 Lovable、Bolt 和 v0 构建的公共仓库,按构建产物而非 README 文本筛选——一个仓库符合条件是因为它包含 .lovable、lovable-tagger 或 .bolt,而不是因为有人在 readme 里写了"使用 Lovable 构建"。每个所有者一个仓库,排除 fork,每人最多三个文件,冻结在 2026 年 9 月 5 日,以确保样本不会在结果之下漂移。
在 1185 个文件中,393 个仓库中有 1163 个是可读的。那 22 个缺失的文件是从冻结以来已在 GitHub 上被删除的。整个运行大约需要 90 秒,不花一分钱,这比听起来更重要——第一次扫描在过程中耗尽了 API 配额。
1163 个文件中的 309 个——占 27%——至少有一条发现。58 个文件有严重问题,分布在 49 个仓库中。按规则来看,每 8 个仓库中就有 1 个存在至少一个严重缺陷。
Cookie set without Secure or SameSite 111
Access-Control-Allow-Origin: * 74
dangerouslySetInnerHTML from a variable 60
RLS policy written as using (true) 39
SQL built by template interpolation 14
innerHTML assigned from a variable 6
Google API key committed in source 3
eval(), weak token randomness, no RLS 7
39 个文件包含一条写成 using (true) 的行级安全策略。
这比单纯忘记启用 RLS 更糟糕,因为这是有人尝试过的 signature。他们读到 Supabase 表需要行级安全性。他们启用了它。然后他们写了一条对每行都返回 true 的策略——这让每次检查都通过了,对每个用户,每次都是。
-- Enabled, and completely ineffective
alter table orders enable row level security;
create policy "read orders" on orders for select using (true);
-- What it needs to say
create policy "read own orders" on orders
for select using (auth.uid() = user_id);
仪表板显示 RLS 已启用。复选框已打勾。该表和以前一样开放。
当被要求"添加 RLS"时,AI 编码工具会愉快地生成第一个版本,因为它是对问题的字面回答。应用中没有任何东西崩溃,没有任何警告,而使表可读的 anon key 按设计被捆绑在前端中。
第一次研究报告称 59% 的扫描仓库存在严重问题。这一次扫描说是 12%。两者都是真实的,而差距才是关键。
在两次扫描都读取的 115 个文件中:
规则只能捕捉语法层面的模式——缺少标志的 cookie、通配符 CORS 头、用模板字面量拼接的 SQL。它们对所有需要理解意图的内容视而不见:一个从不检查请求者身份的端点、用户输入在三个函数之后才到达某个接收点、一个错误处理向浏览器返回堆栈跟踪。
所以 12% 是一个下限,而非估计值。它代表的是仅凭模式匹配而无需理解代码就能证明有问题存在的仓库比例。实际数字更高。我公布这个下限,是因为这是我能逐行捍卫的数字。
这就是当人们发布 LLM 生成的安全统计数据时无人提及的权衡:你得到的是无法复现的广度,或者系统性地少报的严谨性。你无法两者兼得,而你想要哪一个完全取决于是否有人会质疑这个数字。
在 1163 个源文件中,只有三个 secrets 在源代码里被发现。
这听起来像好消息,但事实并非如此。两次扫描都不会打开 .env 文件——而早期研究发现这些仓库中有 18.5% 提交了一个。Secrets 在这些项目中并不是被粘贴到组件里的。它们停留在第一次推送时就进入的环境文件中,而这也正是随意查看代码永远不会找到它们的地方。
搜索你的 SQL 中的 using (true)。如果它位于保存客户数据的表上,整篇文章就在你自己的仓库里。
运行 git log --all -- .env。任何输出都意味着该文件在你的历史记录中。轮换密钥是唯一的修复方法——删除文件不是,因为历史记录保留着旧的 blob。
检查未登录请求能读取什么,而不是信任 RLS "已开启"意味着它在做任何事情。从外部看,一个空表和一个受保护的表看起来完全相同,所以唯一的真正证明是两个账户:以第二个身份登录,然后尝试读取第一个账户的行。
完整的方法、每条规则的计数和注意事项已在 https://www.vibesafe.info/blog/393-ai-built-repos-rules-scan 详细记录。
如果有人想检查工作过程,我很乐意分享每个文件和每个发现的 CSV——每一行都带有仓库、文件、规则 ID 和行号。我不发布仓库名称:这些是真实的人的真实项目,其中大多数人一无所知,而一份易受攻击的应用程序列表就是一份目标列表。