回溯控制与限额保护
回溯式引擎(PCRE、Perl、Python re、Java、JS、.NET)在匹配失败时会回头尝试其他可能,这既带来灵活性,也带来指数级回退的风险:像 (a+)+b 这样的嵌套量词碰上长串非匹配输入,回溯次数可以随长度爆炸,表现为单个请求吃满一颗 CPU。控制手段分三层:改模式、限输入、设限额与超时。
基本信息
| 根本原因 | 嵌套量词与重叠可选项导致同一段文本被反复尝试 |
|---|---|
| 模式层手段 | 去掉嵌套量词、用互斥字符类、原子组与占有量词、锚定与固定前缀 |
| 引擎层手段 | PCRE 系可设 match limit、depth limit;PHP 有 pcre.backtrack_limit 与 pcre.recursion_limit |
| 系统层手段 | 限制输入长度、独立进程或线程执行、超时熔断、用线性时间引擎处理不可信输入 |
典型使用场景
- 解析用户提交的内容:邮箱、URL、Markdown 片段都必须按不可信输入对待
- 批量日志处理:模式一旦低效,海量行数会把耗时放大到不可接受
- WAF 与网关规则:规则由多人维护,需要能明确回溯量的写法
- 长文本抽取:先锚定或先按行切分,再跑结构复杂的模式
- 接口前置校验:把模式收窄到固定前缀,减少无谓的起始位置尝试
改写与限额写法
改写与限额写法
^(\w+\s?)*$ # 差:嵌套量词,长串非匹配输入会指数回溯
^\w+(?:\s\w+)*$ # 好:改成不重叠的两段结构
"(?:[^"\\]|\\.)*" # 好:用互斥字符类代替 .*? 加断言
(?>\w+)[.!?] # 原子组:一旦匹配成功就不再回退
\w++[.!?] # 占有量词:等价于给量词加原子性
preg_match($pat, $s); # PHP:超出限额返回 false,用 preg_last_error() 判断原因
ini_set('pcre.backtrack_limit', '1000000'); # 提高限额只是缓兵之计,优先改模式各引擎的回溯控制与限额
| 引擎 | 原子组与占有量词 | 限额或超时配置 |
|---|---|---|
| PCRE / PHP | 支持 | pcre.backtrack_limit 与 pcre.recursion_limit |
| Python re | 3.11 起支持原子组与占有量词 | 无内置限额,需自行做超时或换库 |
| Java | 支持 | 无内置限额,靠线程超时或改用线性引擎 |
| JavaScript | 不支持原子组与占有量词 | 无内置限额,主线程会被阻塞 |
| .NET | 支持 | 构造 Regex 时可指定匹配超时 |
原子组与占有量词都会取消局部回溯:原本靠回退才能匹配成功的输入会变成不匹配,改写后必须用原有测试样本回归,否则会引入静默的漏匹配。
收到限额或超时错误时不要直接调大阈值:阈值放大只是把 CPU 耗尽的临界点推远,正确做法是重写模式、限制输入长度,并对不可信输入使用线性时间引擎(RE2、Go regexp、Rust regex 一类)。