把根因归到"嵌套的 ."上,这个说法值得商榷。准确讲,触发灾难性回溯的不是嵌套本身,而是嵌套量词所匹配的内容发生了重叠——内层和外层都能吃下同一段字符,于是同一个前缀存在多种切分方式。像 (a+)+ 里,a+ 和外面那个 + 都能吞掉 a,输入一段全 a 又没有结尾 b 时,引擎会把 “aaaa” 拆成 (a)(a)(a)(a)、(aa)(aa)、(aaa)(a)……所有组合都试一遍,复杂度大约 2^n。但这真不是 . 的专利,(a|a)+ 这种 alternation 重叠一样会炸。所以判断风险不能只盯着"有没有嵌套",得看量词之间是否存在可重叠的匹配空间。
严格来说
顺着这个补两点。一是引擎差异被低估了。帖子里给的思路是"加超时"或"上解析器",但还有条更干净的路:换线性时间引擎。RE2(Go 的 regexp 默认就它)和 Rust 的 regex crate 走自动机/DFA 路线,从构造上保证 O(n),根本不回溯,天然免疫 ReDoS。与其给每处匹配套超时——而且超时在回溯引擎里往往是进程级兜底,杀进程时容易把同进程其它在途请求一起带走——不如优先选对引擎。当然 RE2 也会拒绝它编译不了的复杂 pattern,算是一种强制约束。
二是 review 阶段其实有工具兜底。safe-regex、Semgrep 的 ReDoS 规则、CodeQL 都能静态扫出潜在的灾难性模式,不必全靠人眼。再顺手把开放量词换成有上界的写法,外加对单条输入设个最大长度,能挡掉大部分构造型攻击。
你们灰度那次如果当时用的是 RE2 系引擎,大概率连这一课都不用补。话说回来,你们监控是从哪个指标最先闻到味儿的?CPU 打满之前,QPS 或者 P99 有没有先异动?