fix(highlight): ReDoS 检测降级为编辑器提示(实测漏判,不可作拒绝闸门)
上一提交把 isReDoSProne 当作 compileRules 的拒绝闸门,声称"9/9 危险全抓"。
独立复查证伪了这个结论:
## 实测漏判 —— 危险规则仍会进热路径
| pattern | 静态检测 | 实测 |
|---|---|---|
| [(](a+)+$ | 放行 | 30 字符 4065ms |
| (?:a\|a)+$ | 放行 | 26 字符 7547ms |
| ((a\|a))+$ | 放行 | 24 字符 16212ms |
| (\w\|\d)+$ | 放行 | 26 字符 14451ms |
根因三条:外层扫描不跟踪字符类状态([(] 里的 ( 被当组起始)、遇到不闭合
组时 return false 让整个函数交白卷、分支比较只做字面相等抓不到语义重叠。
## 实测误判 —— 合法规则被拒
(?:a{2,3})+ 等把 {n,m} 范围重复当成 {n} 固定重复。
尝试修复后每修一处引入新问题(补字符类状态后又误杀 (?:\d{2})+ 与 ipv4
预设),证明手写正则语法扫描有本质缺陷。
## 三个替代方案均否决(有实测依据)
1. 运行时熔断(循环内查时钟):**原理上无效**。单次 exec 阻塞 28426ms,
时钟检查根本执行不到
2. re2:不支持 lookbehind,perm/okstate/http 三条预设依赖它
3. Worker + 超时:**可靠**(6/6 危险全抓、806ms 内 tick 51 次主线程未阻塞)
但往返 0.11ms/块 vs 直接 0.035ms,且终端高亮必须同步(WriteBatcher 消费
同步染色结果)——只适合规则的异步校验,不适合热路径
## 本次改动
- compileRules 不再因 isReDoSProne 跳过规则:拒绝闸门会静默禁用合法规则
(已证实会误杀)却仍放进危险规则(已证实会漏判),两头都不对
- isReDoSProne 保留给编辑器提示:漏判不比改动前差,误判只是多一个警告而非
禁用功能
- 注释与 AGENTS.md 如实标注"只有提示没有根治",并记录三个方案的否决理由,
避免后来者重复踩坑
热路径的实际边界仍是 MAX_LINE_LEN(>4KB 行不进用户正则)+ MAX_PER_RULE。
真正的解法(Worker 异步校验 + 存盘前把关)列为未完成项。
This commit is contained in:
1 parent
c6aca15134
commit
68fa46b2e3
4 files changed
+66
-29
No files matched your search
+7
-4
@@ -1238,12 +1238,15 @@ console.log('[ReDoS shapes]')
|
||||
const flaggedPresets = DEFAULT_HIGHLIGHT_RULES.filter((r) => isReDoSProne(r.pattern)).map((r) => r.id)
|
||||
ok(flaggedPresets.length === 0, `no shipped preset is rejected (flagged: ${flaggedPresets.join(', ') || 'none'})`)
|
||||
|
||||
// A rejected rule must not reach the compiled set at all.
|
||||
// The heuristic is ADVISORY: it must not gate the hot path. Rejecting on it
|
||||
// would silently disable legitimate rules (measured false positives) while
|
||||
// still admitting hostile ones (measured misses: `[(](a+)+`, `(?:a|a)+`), so
|
||||
// compileRules keeps both and only the editor surfaces the warning.
|
||||
const compiled = compileRules([
|
||||
{ id: 'bad', pattern: '(a+)+$', priority: 1, color: { fg: '#ff0000' } },
|
||||
{ id: 'good', pattern: 'ERROR', priority: 2, color: { fg: '#00ff00' } }
|
||||
{ id: 'flagged', pattern: '(a+)+$', priority: 1, color: { fg: '#ff0000' } },
|
||||
{ id: 'plain', pattern: 'ERROR', priority: 2, color: { fg: '#00ff00' } }
|
||||
])
|
||||
ok(compiled.length === 1 && compiled[0].id === 'good', 'compileRules drops the unsafe rule and keeps the safe one')
|
||||
ok(compiled.length === 2, 'compileRules keeps flagged rules (the check is advisory, not a gate)')
|
||||
}
|
||||
|
||||
console.log(failed === 0 ? '\n[hl-rules] ALL CHECKS PASSED' : `\n[hl-rules] ${failed} CHECK(S) FAILED`)
|
||||
|
||||
Reference in new issue
Block a user