为何指定正则表达式在JavaScript中易受ReDoS攻击但在PHP中却无此风险?
为何指定正则表达式在JavaScript中易受ReDoS攻击但在PHP中却无此风险?
咱们先从你给出的测试结果说起:同样的正则,在Chrome里跑要1秒多,PHP哪怕关掉JIT也只需要8毫秒,差距确实大得离谱。核心原因其实是JavaScript和PHP所用的正则引擎在底层实现、优化策略上有着天壤之别,哪怕正则语义看起来完全一致,执行逻辑却差了很多。
1. 正则引擎的核心类型与回溯处理差异
JavaScript的正则引擎是传统的非确定性有限自动机(NFA),它的特点是遇到分支或重复模式时,会尝试所有可能的匹配路径,一旦匹配失败就会回溯。对于你这个包含多层嵌套重复、复杂先行断言的正则来说,一旦输入触发了大量回溯(比如你构造的超长IPv6字符串),就会陷入指数级的时间复杂度,也就是ReDoS的根源。
而PHP用的是PCRE(Perl Compatible Regular Expressions)引擎,哪怕关闭了JIT,它也有大量针对NFA的优化:
- 原子分组与回溯修剪:PCRE会自动识别一些不会影响匹配结果的回溯路径,直接跳过,避免无效尝试;
- 先行断言的预计算:你开头的两个负向先行断言
(?!...),PCRE会一次性计算完必要的检查,而不是像JS那样在匹配过程中反复重新计算; - 启发式匹配优先:PCRE会优先尝试更可能成功的匹配路径,减少不必要的回溯次数。
2. 针对该正则的具体优化点
咱们看你用的这个邮件验证正则,里面有大量的重复结构和复杂字符类:
- 开头的两个长度限制先行断言,JS引擎会逐字符反复检查是否达到255/65次重复,而PCRE会对这类长度限制做快速预判,不需要逐字符遍历;
- 域名部分的IPv6匹配逻辑,PCRE对括号嵌套、重复模式的处理有专门的优化,而JS在面对超长的重复字符串时,会陷入大量的回溯尝试,导致耗时飙升。
3. 测试代码回顾
先看你在JavaScript里的测试代码:
const re = /(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))/ const s = '0@[IPv6:7:6:' + 'a:1:2:::3:a:a1'.repeat(2000) + '\x00' console.time() re.test(s) console.timeEnd()
结果耗时约1245ms,这就是典型的ReDoS表现——输入触发了大量回溯。
再看PHP的测试代码:
<?php $re = '/(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))/'; $s = '0@[IPv6:7:6:' . str_repeat('a:1:2:::3:a:a1', 2000) . '\x00'; ini_set("pcre.jit", "0"); $start = microtime(true); printf("Result: %d\n", preg_match($re, $s)); $end = microtime(true); if (preg_last_error()) printf("⚠️ Error code: %d\n", preg_last_error()); printf("Time: %d ms\n", ($end - $start) * 1000);
结果仅耗时8ms,哪怕关闭了JIT,PCRE的基础优化已经足以避免ReDoS级别的回溯开销。
总结
简单来说,JavaScript的正则引擎在处理复杂、高回溯风险的正则时,没有PCRE那么多成熟的优化手段,容易陷入指数级回溯;而PHP的PCRE引擎(即使无JIT)通过回溯修剪、预计算、启发式匹配等优化,大幅减少了无效的匹配尝试,从而避开了ReDoS风险。
内容来源于stack exchange
相关产品推荐
相关产品推荐

