PHP preg_match() 默认不防 ReDoS,因 PCRE 的 NFA 引擎天然易受嵌套量词(如(a+)+)、重叠分支(如(x|xx|xxx)+)、贪婪回溯(如^.*.(js|css|png)$配长路径)等结构触发灾难性回溯,必须配合 pcre2_set_match_limit 等限流措施。

PHP 的 preg_match() 默认不防 ReDoS,只要正则含嵌套量词或重叠分支,配合恶意输入就能让单个请求吃光 CPU——这不是配置问题,是引擎机制决定的。
哪些 PHP 正则结构会触发灾难性回溯
PHP 使用 PCRE(Perl Compatible Regular Expressions),底层是传统 NFA 引擎,天然支持回溯,也天然容易被击穿。以下结构在真实日志和 WAF 规则中高频出现,且已验证可导致秒级甚至分钟级阻塞:
-
(a+)+、(.*a)*、([^"]*)*:嵌套量词,输入每多一个a或",匹配时间约翻倍 -
(x|xx|xxx)+、(\d|\d\d|\d\d\d)+:重叠可选分支,引擎需穷举所有等价切分方式 -
^.*\.(js|css|png)$配合/static/a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.js:贪婪.*吃光后逐个吐出点号尝试后缀,路径数爆炸 -
.*X.*Y.*Z(尤其 X/Y/Z 位置模糊):典型“模糊锚定失败”场景,不匹配时回溯深度随字符串长度指数增长
preg_match() 必须配超时,否则等于裸奔
PHP 原生 preg_match() 没有超时参数,set_time_limit() 对 PCRE 内部回溯无效。必须用 pcre2_set_match_limit() 和 pcre2_set_depth_limit()(PHP 8.2+)或封装信号中断(PHP
- PHP 8.2+ 推荐方式:调用
preg_match()前,用ini_set('pcre.backtrack_limit', '100000')和ini_set('pcre.recursion_limit', '1000')降级限制(注意:这两个 ini 值全局生效,需谨慎) - 更安全做法:改用
PCRE2扩展(非默认),显式设置pcre2_set_match_limit(),例如限制最多 10 万次回溯 - PHP 7.x / 8.0–8.1:只能靠
pcntl_alarm()+pcntl_signal()实现硬超时,但需启用pcntl扩展且不能用于 Web SAPI(如 Apache mod_php)
写正则时能绕开 ReDoS 的三个硬约束
防御不能只靠运行时拦截,源头写法才是成本最低的防线。以下不是“建议”,而是经压测验证有效的强制约束:
立即学习“PHP免费学习笔记(深入)”;
- 禁用连续贪婪量词:
.*.*、.+.+、[a-z]*[a-z]*—— 改用[^/]{0,256}、[^"\n]{1,128}等明确字符集 + 长度上限 - 所有用户可控输入必须转义:
preg_quote($user_input, '/'),否则$user_input = '.*'直接变成高危模式 - 路由 / 校验类正则必须锚定:
^开头、$结尾,避免引擎在长文本中多位置试探;preg_match('/^\/api\/v\d+\/.+$/', $path)比'/\/api\/v\d+\/.+/'安全得多
CI/CD 中必须加入的静态检测环节
人工 review 正则极易漏掉嵌套结构。上线前不扫描,等于把钥匙交给攻击者:
- 用
redos-detector(Python 工具)跑 CI 流程,自动识别(a+)+、(.*a)*等 AST 模式,失败即阻断构建 - 对所有暴露接口的正则(如 API 参数校验、Nginx
location ~、Laravel 路由约束)做模糊测试:输入 50 个a加结尾干扰字符,响应 > 100ms 就标为可疑 - 禁用
preg_replace()动态拼接:如preg_replace('/' . $_GET['pattern'] . '/', ...)是典型反模式,必须拒绝
ReDoS 不是“可能出问题”,而是“只要条件凑齐就必然爆发”。最常被忽略的是:开发环境用短测试数据看不出问题,一到生产面对真实长 URL 或日志字段,CPU 就拉满。别依赖“我们没写那么烂的正则”,要依赖“所有正则都过扫描 + 所有匹配都带限流”。



















