一、起点:报告被 triage 判了 NA
我在 Bugcrowd 上投了个漏洞报告,隔天收到回复,核心两句:
unable to reproduce the described issue content appears to be low-quality or potentially automated (AI-generated)
翻译一下:复现不了 + 看着像 AI 生成的。
第一句我能忍,第二句真扎心。所以我回头做了一次系统复盘,给自己的流水线加了五道改造,其中一道是------AI 味检测器。
二、我"修好"了它,还有实测数据
检测器的逻辑很直白:扫描报告正文,命中模板化套话/模板标题就扣分,压不到 0.6 以上不许提交。
升级点有三个,我列出来是为了后面打自己的脸:
- 套话库补充(
materially reduces the cost、collectively this、attack-surface map这类 triager 一眼能识别的句式) - 模板标题检测大小写不敏感 ,并且容忍
## Summary这种带 markdown 前缀的写法 - 加粗密度检测(满屏
**xxx**是 AI 排版特征)
改完我跑了对照,数据漂亮:
| 稿件 | 分数 |
|---|---|
| 旧稿(AI 味重) | 0.5 ✅ 被拦 |
| 新稿(改过的) | 1.0,零命中 ✅ 过闸 |
我当时挺满意:"区分度出来了。"
三、然后有人让我复跑一遍
我同事(一个不会累、不会不好意思的家伙)说:报告里的数我不接受转述,重新跑一遍对账。
行,跑。跑完他问了一句:
四份稿件的"模板标题"命中数,全是 0。你觉得这正常吗?
我一看,还真是。连那份明明白白写着 ## Summary、## Impact、## Remediation 的旧稿,模板标题命中也是 0。
也就是说:我升级的那条规则,一分力都没出。 旧稿那个 0.5,完全是靠"套话短语 + 加粗密度"两项扣出来的。模板骨架检测------全程空转。
而新稿那个"1.0 零命中",是漏检漏出来的满分,不是干净。
四、根因:正则少一个 flag
代码长这样:
python
_TEMPLATE_HEADER_PAT = re.compile(
r"^[ \t#*\-]*(" + "|".join(re.escape(h) for h in AI_TEMPLATE_HEADERS) + r")[ \t]*:?\s*$",
re.IGNORECASE, # ← 我加了大小写不敏感
)
问题在 ^ 和 $。
Python 的 re 默认不开 re.MULTILINE 时,^ 和 $ 只锚定整个字符串的首尾。 而报告正文是一整篇多行文本------所以这个正则表示的是"整篇报告必须恰好只有 一个标题行",永远匹配不到夹在中间的 ## Summary。
我加对了 IGNORECASE,却漏了真正关键的那个。
实测三行,证据确凿:
sql
正则 flags 含 MULTILINE → False
正则在多行报告上 search() → False ← 漏检
同一正则手动补 re.M 后 → True ← 本来能中
五、还藏着一个更辣的
修 MULTILINE 之前,我先把循环读了一遍:
python
for h in AI_TEMPLATE_HEADERS:
if _TEMPLATE_HEADER_PAT.search(text):
cliche_hits.append("模板标题: " + h)
看着没问题?一旦正则会中了,这个循环会把名单里 14 个标题全部 append 一遍 ------因为它判断的是"文本里有没有任一模板标题",中的时候对每个 h 都为真。
也就是说:我要是不修 MULTILINE,直接补 flag,闸门会从"全程空转"变成"一碰就扣满 0.5"。两个 bug 互相掩着,一个装死、一个埋伏。
正确写法是对实际命中的结果去重:
python
for _h in sorted({m.group(1).lower() for m in _TEMPLATE_HEADER_PAT.finditer(text)}):
cliche_hits.append("模板标题: " + _h)
六、修完的复测
| 稿件 | 修前 | 修后 |
|---|---|---|
| 旧稿(AI 味重) | 0.5(模板标题 0 条) | 0.5(模板标题 3 条 + 套话 5 条) |
| 我 10-12 的定稿 | 0.9 | 0.6 |
| 新稿 v2 | 1.0(假满分) | 0.8(命中 2 条模板标题) |
最后一行是重点:那个"1.0 零命中"是假的。 修好之后它掉到 0.8,因为它的 Suggested fix 和 What I found 两个标题本来就在黑名单里,只是检测器当时瞎了。
七、我学到的三件事
1. "改好了"这三个字,必须拿实测数据换,尤其是你自己写的校验逻辑。
我这次是怎么翻车的?不是没测,是测的时候只看了总分。0.5 和 1.0 都出来了,看着有区分度,就没拆开看"这分是谁扣的"。如果当时多打印一行命中明细,第一条规则空转当场就暴露了。
2. 一个"只会放行"的检查器,不是检查器。
它全程说 yes,你说它没问题。真正好用的闸门,得先证明它能说不------拿已知的坏样本去撞,撞得停才算数。后来我给这套检测器补了夹具:旧稿必须 0.5、v2 必须 1.0,每次改完跑一遍。
3. 两个 bug 会互相掩护。
空转的那个让埋伏的那个永远不暴露。只看"现在能不能用"是看不出来的,得看"每一条规则分别出了多少力"。
你们写校验逻辑的时候,会拆开看每一条规则各自的命中数吗? 还是只看最终分数?
我现在的要求是:任何"我修好了"的结论,必须附带一张"每条规则分别命中几条"的表。不然我自己都不信------这次就是活生生的例子。