flawopen.com/ReDoS/Explained
什么是 ReDoS(灾难性回溯)?
支持回溯的正则引擎在匹配失败后,可能会尝试以不同方式拆分同一输入。某些含糊模式的尝试次数会随输入长度呈指数增长。实际延迟取决于正则、引擎、输入长度、并发量和资源限制;重复的高开销请求可能耗尽服务容量。
# Nested quantifier ^(a+)+$ # Overlapping alternatives, quantified ^(a|aa)+$ # A long matching prefix plus a final mismatch can # force extensive retries in vulnerable engines. # Do not benchmark this with attacker-sized input.
# Bound input before matching, then use a simple # full-string identifier rule with no nested ambiguity. import re IDENTIFIER = re.compile(r"[a-z0-9]{1,64}\Z") def valid_identifier(value: str) -> bool: if not isinstance(value, str) or len(value) > 64: return False return IDENTIFIER.fullmatch(value) is not None # A bound reduces worst-case work; still set request # size, concurrency, and execution-time limits.
回溯为何会激增
在回溯引擎中,像 ^(a+)+$ 这样的模式存在歧义:序列 aaaa 可以用多种方式分配给嵌套量词。若末尾字符不匹配,引擎可能需要尝试多个分支,才能判定整个字符串失败。
对于存在这种歧义的模式,匹配前缀后跟不匹配字符时,探索路径数可能随前缀长度呈指数增长。这取决于具体的模式与引擎组合,并非所有正则或失败匹配都会如此。并发请求下,较短输入也可能产生影响;应限制输入长度、并发量和执行时间。
不可信输入会从哪里进入正则
输入校验是显而易见的入口,但风险也可能藏在其他地方:解析 User-Agent 等请求头、日志处理、Markdown 与语法高亮、URL 路由、内容过滤器和 WAF 规则,以及对传入字段执行正则的库。许多常用软件包曾发布 ReDoS 公告,因此依赖树和自有模式同样需要审查。
常见问题
风险取决于引擎和模式,而不只是编程语言。某些回溯引擎会在特定模式上进行过多尝试。Go 的 regexp 文档保证运行时间与输入长度呈线性关系。Rust 的 regex crate 对单次搜索给出 O(m·n)(m 为模式长度,n 为输入长度);模式固定时,对输入长度为线性。重复搜索迭代器的最坏复杂度不同。这些引擎不支持反向引用等会妨碍此类保证的功能。
长度限制可以限制匹配的工作量,但安全上限取决于模式、引擎、请求速率和服务预算。还应修复含糊模式,并在解析或匹配前应用限制。
使用支持 ReDoS 检查的静态分析器,并结合实际使用的引擎审查结果。检查包含重复量词的重复组,或前缀重叠的分支;随后在隔离环境中用有界输入和严格超时进行测试。
超时支持取决于运行时和 API。在 .NET 中,对不可信输入的匹配应明确设置超时。其他环境要查阅具体引擎文档;若没有可用超时,应采用更安全的引擎或重写模式。超时应与输入和并发限制配合使用,不能让含糊模式本身变高效。
参考资料
- CWE-1333 — Inefficient Regular Expression Complexity
- OWASP — regular expression denial of service
- Go regexp package documentation
- Rust regex crate — performance and complexity
- .NET regular expression best practices
- Russ Cox — regular expression matching can be simple and fast
- Is JavaScript's RegExp vulnerable to ReDoS?