我的安全审计脚本,把自己的修复当成了攻击

上线第三天,自动化巡检跑完给我发来一条告警:
检测到敏感信息泄露,请立即处理。
我打开一看,问题出在我刚刚修复过的文件里。我手动把一个旧 API Key 替换成了脱敏占位符,保存,提交,脚本扫了一眼,判定——你在泄露密钥。
我没有泄露。我在修复泄露。
但脚本不知道。
一个无差别匹配的安全扫描器,会把你的修复动作本身判定为攻击。这是工具设计的根本性错误,不是参数问题。
这是一个用一行正则解决的 Bug,但它背后的教训值得说清楚。
从”删除行”里找到的幽灵
先把问题讲明白。
Git 的 diff 格式大家都见过:加号开头是新增行,减号开头是删除行。
当我脱敏一个文件,把 sk-abcdefg... 改成 sk-xxx... 之后,git diff --cached 会输出这样的内容:
1 | - "api_key": "sk-abcdefg..." |
减号行是我要删掉的旧内容,加号行是我写进去的新内容。
安全审计脚本是这么工作的:扫描整个 diff 文本,只要匹配到密钥格式的正则,就报告”发现密钥”。
它看到了减号行里的旧 Key,然后……报警了。
脱敏操作本身触发了安全告警。这就是这个审计脚本三个月来的工作状态——每次修复旧泄露,它都会把这次修复当成新的攻击来告警,形成一个永远无法收敛的误报循环。
一行正则,切断误报循环
修复极其简单。
1 | # ❌ 旧版:无差别扫描整个 diff 文本 |
就这一个条件判断。
但它解决的问题是:扫描器的目的是发现”我往代码里写进了什么”,不是发现”我从代码里删掉了什么”。 删掉的旧密钥,恰恰说明修复在生效。
这个 Bug 之所以能存活三个月,是因为它每次触发的时机太特殊——只有在你做脱敏修复时才会误报,而做脱敏修复本身是低频操作。低频 Bug 是最难被发现的 Bug。
另一个同天发现的陷阱:Git 分叉
同一次巡检里,我还发现了第二个故障:自动同步脚本连续两天被 Git 拒绝了。
报错是 rejected: non-fast-forward。
根因是这样的:我在某次手动操作中直接 push 了一个提交,但定时任务里的同步脚本并不知道远端已经领先了。它拿着本地的旧状态去推,自然被拒。
很多人遇到这种情况会选择 git push --force。我没有这么做,因为强推会覆盖远端历史,而远端的那个领先提交是我手动推上去的——是真实有效的历史,不是垃圾。
正确的修复是在每次 push 前先同步一次:
1 | # 同步脚本新增:push 前先拉取远端最新状态 |
fetch + rebase 的组合,保证本地历史在远端最新状态之上,push 永远是 fast-forward,永远不会被拒绝。
两个故障的共同根因:脚本被写成了”假设世界不变”的样子,但世界一直在变。安全审计假设 diff 里出现的 Key 都是新写入的;同步脚本假设本地永远是最新的。现实打了它们的脸。
工具是写给特定假设的,假设一旦不成立就会出错
安全审计脚本误报,本质是设计时的隐含假设出了问题:”diff 里出现的密钥 = 正在写入的密钥”。这个假设在 99% 的情况下成立,但在”正在做脱敏修复”这个场景下直接失效。
Git 同步脚本失败,本质也是假设失效:”本地是最新的”。手动操作打破了这个假设。
解法从来不是把假设藏得更深,而是明确说出假设,并为假设失效的情况写处理逻辑。
一行 if line.startswith('+') 就是在说:我知道删除行也可能有 Key,但我不在乎,因为被删掉的东西不是威胁。
一段 fetch + rebase 就是在说:我不假设本地最新,每次同步前先确认一次。
工具越聪明,隐含假设越多,越容易在边缘场景翻车。把假设写清楚,才是让工具真正可靠的方式。
这套思路能用在任何你维护的自动化脚本上,不只是 Git 和安全审计。
你的自动化脚本里,还有多少”假设世界不变”的地方?