preg_match()返回false表示执行出错而非未匹配,最常见是回溯超限;应通过preg_last_error()确认错误类型,安全写法需显式判断===0才代表未匹配成功。

preg_match()返回false不等于“没匹配上”
很多人把 preg_match() 返回 false 当成“输入不合法”,直接放行或跳过校验逻辑,这是最危险的误判。它其实只表示函数执行出错——而最常见的错误就是回溯超限(PREG_BACKTRACK_LIMIT_ERROR),不是规则失效,是引擎崩了。
典型表现:构造一个长字符串触发大量回溯,比如对 /^a.*b$/ 输入一万个 a 后接 c,preg_match() 就会因回溯次数超过 pcre.backtrack_limit(默认 100 万)而返回 false,而非 0。
- 用
var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR)确认是否真为回溯超限 - 不要只写
if (!preg_match(...))—— 这会把false和0一起放过 - 安全写法必须显式区分:
if (preg_match(...) === 0)才代表未匹配成功
正则里 .* 和 .+ 是回溯炸弹温床
无界量词(.*、.+、[a-z]*)配合后续必须匹配的字符,极易引发灾难性回溯。比如 /^,当输入是 <code><?php ... 很多无关字符 ... x(结尾不是分号),引擎会反复尝试不同截断点,直到耗尽回溯额度。
真实绕过场景常见于 WAF 或白名单过滤:规则想拦 <?php ,却写了宽松正则,攻击者塞入超长干扰字符就可让 preg_match() 失效。
立即学习“PHP免费学习笔记(深入)”;
- 避免在非必要位置使用
.*;改用更精确的字符类,如[^;\n\r]* - 若必须用通配,加上占有性量词(PHP 7.3+):
.*+或.++可禁用回溯 - 测试时用极端输入验证:生成 10k 长度的随机字符串 + 不满足结尾条件,看是否返回
false
offset 参数能绕过前半段匹配检查
preg_match() 的 offset 参数指定从第几个字节开始搜索,但开发者常忽略它会影响锚点行为。比如规则是 /^admin$/i,本意是整串必须等于 admin,但如果调用时传了 offset=1,那 preg_match() 就从第二个字符开始匹配,^ 实际锚定到偏移后的位置,导致 xadmin 在 offset=1 时也能匹配成功。
这在参数拼接或二次解析场景中很隐蔽,尤其当 offset 来自用户可控输入(如分页参数、切片索引)时。
- 除非明确需要局部匹配,否则不要传
offset;传的话务必做范围校验(如max(0, min($offset, strlen($subject)))) -
^和$在带offset时不再代表整个字符串首尾,而是当前搜索起点和末尾 - 用
mb_substr()截取后再调用preg_match(),比依赖offset更可控
数组传给 subject 会导致静默失败
当 $_GET['param'] 是数组(如 ?param[]=1¶m[]=2),直接传给 preg_match('/^\d+$/',$input),函数会因类型不符返回 false,而不是报错或转换。很多旧代码没做 is_string() 检查,就把这个 false 当成“没匹配上”,结果让数组绕过所有正则校验。
这种绕过不依赖复杂 payload,只要发个数组就行,是 CTF 和真实漏洞中最廉价有效的突破点之一。
- 始终在调用前加
if (!is_string($input)) { die('invalid input'); } - 注意
$_REQUEST、$_POST、$_GET中任意键都可能被构造为数组 - PHP 8.1+ 对非字符串
subject会发出E_WARNING,但默认不中断执行,不能依赖告警做防御
回溯绕过真正的难点不在构造 payload,而在理解 preg_match() 的三态返回(1/0/false)如何被业务逻辑错误解读。哪怕正则本身再严谨,只要判断分支漏掉 false 分支,或者把数组当字符串处理,防线就形同虚设。



















