最近在给一个安全产品配置一个正则,有趣的是,回溯历史数据的时候发现,有一些字符串会导致引擎超时,从而触发熔断机制,导致策略失效。经过简单的测试,发现是触发了 redos。
我本来以为我对正则非常熟悉了,不会写出这么挫的正则,但的确发生了。虽然 redos 之前一直知道是怎么个事,还是想借此机会再完整地梳理一下,故有此篇。
ReDoS(Regular Expression Denial of Service) 是一种利用正则表达式设计上的缺陷来耗尽计算资源的攻击形式(即 DoS)。这种攻击的目标通常是那些采用了回溯机制的正则引擎
这里稍微回顾一下编译原理的知识点,非常建议看一下这篇文章:编译原理(一):词法分析/#词法分析器的实现
Thomson 算法。基本方式是递归。构造完成之后,执行时根据输入字符串执行状态转移,逐步计算每个状态集合是否到达接受状态,不需要回溯对于正则表达式本身的基础知识不再赘述,网上一搜一大把,这里核心是这几个贪婪模式(实际上都是正则的语法糖):
c+:告诉引擎匹配 c >=1 次c*:告诉引擎匹配 c >=0 次c{min, max}:告诉引擎匹配 c >=min and <=max 次;min/max 和逗号可以按需省略这里对下述内容做个约定:
a(b|c)* 其实是 ^a(b|c)*$大部分正则引擎在匹配正则表达式时,会尝试所有可能的路径来寻找匹配结果。当某个路径失败时,引擎会回退到之前的状态来尝试其他路径(即会出现“回溯”),是一个深度优先的遍历算法。
为了验证这一点,我们可以找个在线可视化的正则匹配网站来测试:regex101,这里面有个

例如这个正则 a(b|c)*,当输入 a 时:
a,这个很简单
(b|c)* 这个结构,产生匹配,显然这个是匹配不到的
(b|c)* 没有匹配到字符,但由于后续也没有其他字符了,所以结束,匹配成功从调试的步骤可以看出,这个正则是存在回溯的。
至此,redos 的原理就显而易见了:由于大部分正则表达式引擎是基于 NFA 的,如果正则中有大量的回溯结构,遇到较长且无法匹配的字符串时,就会触发大量的回溯计算,导致计算资源的占用,进一步导致各种安全问题。
例如这个经典的正则 (a|aa)*b,随着输入字符串的连续 a 数量增多,匹配完成的耗时也就越长:
1 | |
运行情况:


典型的指数型增长。
不过我觉得只要有这种回溯的结构,理论上只要输入字符串够长就会有 redos,不过利用难度会变高:

所以我们基本上可以认为,回溯结构的多少以及复杂程度,与 redos 的利用成本成反比。
那如果使用非贪婪模式的正则,能不能解决这个问题呢?所谓非贪婪模式,或者懒惰模式,就是在贪婪模式后面加个 ?,例如 a+?。显然这个无法完全解决问题:

可以看到,有 redos 正则的核心特征是 *、+、{n,m} 的使用(但使用了这类量词不一定就有 redos),尤其是再配合上嵌套((a*)*b)重叠分支(a|aa)、零宽断言(?=)等语法,会极大增加存在 redos 的可能性。
那是否有什么办法可以评估一个正则是否存在 redos 的可能性?在确定有 redos 的情况下,有没有可能自动构造出 poc 呢?
这里推荐两个比较好的工具:
至于该如何实现呢?根据论文 Static detection of DoS vulnerabilities in programs that use regular expressions 可以得出,基于自动机理论的 ReDoS 漏洞检测是在等效的 NFA 中找到 EDA(Exponential Degree Ambiguity) 和 IDA(Infinite Degree Ambiguity) 结构。
出于时间原因,这里暂时不复现了,看论文还是很麻烦的。
其实核心就是如何避免掉大量的回溯
Cloudflare ReDoS 中断:2019 年 7 月 2 日 Cloudflare 在 WAF 中部署了一项新规则,其中包含的一个正则表达式出现了 redos,耗尽了用于 HTTP/HTTPS 服务的 CPU,导致了 Cloudflare 的核心代理、CDN 和 WAF 等功能宕机近半小时
主要问题出在这个正则:
1 | |
核心部分是:?:.*=.*。通过工具可知,这个正则的确是有 redos 的

经过耗时测试,的确是多项式级的复杂度:

有趣的是,Cloudflare 指出,内部对于这个正则其实进行了大量的测试,但都没有触发 redos,所以这个正则就这么被合并上生产了。
另外,cloudflare 的报告中有个蛮酷炫的正则 debug 的动图:

稍微研究了一下发现是 perl 的一个模块支持的:perl -MRegexp::Debugger -e "'x=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx' =~ /.*.*=.*/",执行之后按 c 就可以进行匹配演示了。需要安装一下 Regexp::Debugger 模块:cpan Regexp::Debugger。perl 我也就半吊子水平,这里就不深入研究它的其他用途了。
没想到
编译原理的知识点还真用上了