← 中文资源

简体中文版本

正则灾难性回溯 — ReDoS 使服务冻结

通过移除嵌套量词、限制输入长度,并使用原子/占有量词与线性时间替代,修复 ReDoS。

阅读英文原文

症状

本篇调试指南针对“正则灾难性回溯 — ReDoS 使服务冻结”。内容涵盖可观察的症状、可复现的诊断路径、根本原因、具体修复方式,以及如何验证修复并防止回归。

可观察到的症状包括:

  • 带构造的长字符串请求使 CPU 满载
  • 正则匹配时间随输入长度指数增长
  • 服务对某些用户输入挂起,其余正常

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

node -e "const r=/^(a+)+$/; const s='a'.repeat(30)+'!'; const t=Date.now(); r.test(s); console.log(Date.now()-t, 'ms')"

耗时随长度急剧增长即确认灾难性回溯。

步骤 2

grep -rnE "\([^)]*[+*][^)]*\).*[+*]" src/ | head

组内量词又被外层量词限定的模式通常是元凶。

常见诱因(按出现频率排序):

  • 嵌套量词如 (a+)+ 产生指数级的回溯路径
  • 重叠的选择分支允许大量等价匹配
  • 未限制输入长度,使攻击者可提供最坏情况字符串

根因

回溯引擎会探索模式匹配的每种方式。嵌套量词使这些方式呈指数增长,因此精心构造的输入会使匹配时间爆炸并耗尽 CPU,形成拒绝服务。

修复

移除嵌套量词,限制输入长度,在支持时使用原子组或占有量词,锚定模式,并对面向用户的正则改用线性时间引擎(RE2)。

注意:看似更简单的模式仍可能回溯;应针对最坏输入测试任何面向用户的正则。

// ❌ nested quantifier -> ReDoS
// const bad = /^(a+)+$/;

// ✅ no nested quantifier, anchored, bounded
const good = /^a+$/;
const bounded = (s) => s.length <= 1000 && good.test(s);

// validate length before matching user input
if (input.length > 255) throw new Error('input too long');

// or use a linear-time engine (RE2) for untrusted patterns

验证

重新计时最坏输入并确认在有界时间内完成;用超长与对抗性字符串模糊测试确认不挂起。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。