引言:一把双刃剑
正则表达式(Regex)是文本处理的神器,但如果使用不当,它也可能成为系统的“定时炸弹”。在 Web 服务中,一个设计拙劣的正则表达式可能导致 CPU 飙升至 100%,甚至引发服务雪崩。这种现象通常被称为 ReDOS (Regular Expression Denial of Service)。
本文将从原理出发,结合我在生产环境中遇到的真实案例,探讨如何优化正则性能以及如何防御 ReDOS 攻击。
一、 灾难的根源:回溯 (Backtracking)
大多数主流语言(Java, Python, JavaScript, C#, PHP)默认使用的正则引擎都是基于 NFA(非确定性有限自动机) 的回溯算法。
什么是回溯?
简单来说,当正则引擎在匹配过程中遇到“选择”或“量词”时,它会尝试一条路径。如果这条路径走不通,它会退回到上一个分叉点,尝试另一条路径。
噩梦场景:指数级爆炸
考虑正则表达式 (a+)+b 匹配字符串
aaaaaaaaaaaaaaaaaaaa (20个 a,后面没有 b)。
- 引擎首先尝试贪婪匹配,
(a+)吃掉所有a。 - 外层的
+尝试重复,但没字符了。 - 发现后面需要
b,但没有b。 - 回溯:内层的
a+吐出一个a,外层的+尝试再匹配一次… - 这个过程会产生大量的组合。对于长度为 \(n\) 的字符串,复杂度可能达到 \(O(2^n)\)。
这就是 ReDOS 的原理:攻击者构造一个特定的“恶意字符串”,利用存在缺陷的正则表达式,让服务器陷入死循环般的计算中。
二、 生产环境实战:一次 CPU 100% 的排查
事故背景
某天下午,我们的日志分析服务突然报警,CPU 持续 100%,处理延迟从毫秒级飙升到分钟级。重启服务后,运行几分钟又重现。
排查过程
定位热点:使用
perf(Linux) 或pprof(Go) 查看 CPU 热点。发现 CPU 主要消耗在正则匹配函数上。定位代码:找到相关业务代码,发现是用于解析 User-Agent 的一段逻辑。
定位正则: 有问题的正则大概长这样(简化版):
User-Agent: .*(\w+-\w+)+.*意图是匹配类似
Product-Name这样的字段。复现: 当 User-Agent 非常长且包含大量类似格式但不完全匹配的字符串时,回溯次数爆炸。 攻击者(或者只是一个异常的爬虫)发送了一个长达 5KB 的 User-Agent,里面充满了
abc-def abc-def ...但结尾不符合预期。
解决方案
- 临时止血:限制 User-Agent 的最大长度,超过 500 字符直接截断或丢弃。
- 优化正则:
- 移除嵌套量词:
(\w+-\w+)+是典型的嵌套量词(Nested Quantifiers)。改为非嵌套形式。 - 使用具体字符类:把
.*换成更具体的[^ ]*,减少尝试范围。
- 移除嵌套量词:
- 最终方案: 将该服务的正则引擎切换为
RE2 (Go 默认就是 RE2,但当时是一个 Python
服务,我们引入了
google-re2库)。RE2 不支持回溯,保证线性时间复杂度,彻底根除了 ReDOS 风险。
三、 性能优化技巧
除了更换引擎,我们在编写正则时也可以遵循以下原则来提升性能:
1. 避免嵌套量词 (Avoid Nested Quantifiers)
坏味道: - (a+)+ -
(\w*)* - (a|b)+ (如果 a 和 b
有重叠)
优化: 尽量展开,或者确保内层和外层不会对同一个字符进行“争抢”。
2. 独占模式 (Possessive Quantifiers) 与 原子组 (Atomic Grouping)
如果你确定匹配了就不需要回吐,可以使用独占模式(Java/PHP
等支持)。 - a++:匹配尽可能多的
a,绝不回溯。 -
(?>...):原子组,组内的匹配一旦完成,就锁定,不回溯。
注意:Python re
模块默认不支持这些特性,需要第三方库
regex。
3. 锚点 (Anchors)
尽量使用 ^ (开始) 和 $ (结束)
锚点。
如果正则引擎知道必须从字符串开头匹配,它这就不会在字符串的每个位置都尝试一遍。
4. 懒惰匹配 (Lazy Matching) 并不总是更快
.*?
是懒惰匹配。虽然它看起来“匹配得少”,但它可能导致引擎频繁地“匹配-暂停-检查后面-回溯-继续匹配”。在某些情况下,贪婪匹配
.*
配合回溯反而更快(因为它一次性吃掉所有,只在最后回溯一次)。
关键在于:你要匹配的内容在字符串中出现的概率和位置。
四、 防御策略总结
- 输入验证:限制输入字符串的长度。ReDOS 需要长字符串才能显现威力。
- 设置超时:在执行正则匹配时设置超时时间(例如 100ms)。如果超时,直接抛出错误。
- 使用安全引擎:如果不需要反向引用等高级特性,优先使用 RE2 或 Rust regex 等基于 DFA/NFA 混合且保证 \(O(n)\) 的引擎。
- 代码审查:在 Code Review 阶段重点关注包含嵌套量词的正则表达式。
- 工具检测:使用工具(如 safe-regex 或在线 ReDOS 检查器)扫描代码库。
结语
正则表达式是强大的,但也是危险的。作为开发者,我们不能只关注“能不能匹配上”,更要关注“匹配失败时会发生什么”。理解回溯原理,是写出高性能、高安全正则的第一步。