当用户需要搜索和替换文本时,正则表达式就会派上用场。然而,在某些情况下,它们可能会导致系统变慢,甚至容易受到ReDoS攻击。
ReDoS是DoS攻击的一种子类型。ReDoS攻击的目的是通过低效的正则表达式停止应用程序或使其变慢。
ReDoS攻击分为两种类型:
(1)将带有恶意模式的字符串传递给应用程序。然后,这个字符串被用作正则表达式,从而导致ReDoS攻击。
(2)将特定格式的字符串传递给应用程序。然后,这个字符串由一个易受攻击的正则表达式计算,从而导致ReDoS攻击。
任何ReDoS攻击的要点都是在应用程序中使用易受攻击的正则表达式。将某种格式的字符串传递给正则表达式会导致其计算时间过长。
如果ReDoS攻击成功,则正则表达式计算将导致灾难性的回溯。这是正则表达式引擎中回溯函数的结果,该函数遍历可能的字符串匹配,直到找到正确的字符串。如果没有正确的匹配,正则表达式将不会停止,直到遍历所有可能的选项。而所有可能选项的完整迭代将导致正则表达式计算的时间过长。这被称为灾难性回溯。
如果正则表达式包含至少一个可能导致大量匹配选项的子表达式,则它很容易发生灾难性的回溯。
以下检查几个正则表达式的漏洞。
在这里编写了一个小程序,它显示了正则表达式的计算时间如何依赖于计算字符串中的字符数的图形。在接下来的示例中,将使用这个程序展示灾难性的回溯。
示例1
以下看一个简单的合成例子:
(x+)+y
比较一下(x+)+y表达式在两种情况下的计算时间:
(1)正则表达式的输入接受与指定模式一一对应的字符串。同时,每个后续字符串的长度都比前一个字符串多一个字符。
(2)正则表达式的输入接受不匹配模式的字符串(字符串末尾没有y字符)。同时,每个后续字符串的长度都比前一个字符串多一个字符。
实验结果如下:
图1字符串匹配模式(x+)+y的正则表达式的执行时间
图2字符串不匹配(x+)+y模式(在结尾缺少y字符)的正则表达式的执行时间
由上可见,第一组字符串立即被处理。然而,第二组的处理速度呈指数级增长!为什么会这样?
问题是,在第一种情况下,正则表达式在第一次尝试时就找到了匹配项。在第二种情况下处理字符串时,一切都变得非常复杂。x+模板可以匹配任意数量的x个字符。(x+)+模板可以适合由一个或多个对应于x+的子字符串组成的字符串。因此,有许多选项可以将字符串与正则表达式匹配。它们的数量取决于由x个字符组成的子字符串的长度。每当正则表达式没有找到y字符时,它就开始检查下一个选项。只有在检查了所有这些之后,正则表达式才会给出答案——没有找到匹配项。
下表显示了xxxx字符串与(x+)+y正则表达式的几种可能匹配:
幸运的是,并非所有正则表达式都容易受到灾难性回溯的影响。如果正则表达式满足以下条件,则会受到ReDoS攻击:
(1)有两个子表达式,其中一个子表达式包含另一个子表达式。此外,以下量词之一应用于它们中的每一个:“*”、“+”、“*?”、“+?”、在前面的示例中,(x+)+子表达式包含x+。
(2)有一个字符串可以与两个子表达式匹配。例如,字符串xxxx可以同时适合x+和(x+)+模板。
(d?|....|[1-9])+类型的表达式是一个小例外。这里的(d?|....|[1-9])+表达式包含子表达式d?和(1-9)。它们通过'|'运算符枚举。这些子表达式也可以适合相同的字符串,例如111。在本例中,应用'?的量词到子表达式之一也会导致漏洞。
结果发现(x+)+y表达式是脆弱的。现在稍微改变一下,添加一个检查另一个字符的存在:
(x+z)+y
现在有了(x+z)+子表达式,xz和xxxxz字符串可以与这个表达式匹配。这个子表达式包括x+子表达式,它可以对应于x、xxxx等字符串。正如人们所看到的,这些子表达式不能与相同的值匹配。因此,即使不满足第二个条件,也不存在灾难性的回溯。
图3使用一组字符串“中断”正则表达式的尝试失败。它们中的每一个都对应于x+子表达式或(x+z)+子表达式。
现在看看下一个正则表达式:
newDate((-?d+)*)
这个正则表达式有一个任务——搜索newDate(12-09-2022)类型的子字符串。能说这个正则表达式是安全的吗?不。除了正确的字符串,正则表达式还会考虑纠正newDate(8-911-111-11-11)甚至newDate(11111111111)字符串。然而,要理解问题的本质,这样的表达就已经足够了。
上述选项都不会导致灾难性的回溯。然而,如果处理“newDate(1111111111111)”类型的字符串,就会发生这种情况。
图4正则表达式检查与模式不匹配的字符串的执行时间(字符串末尾没有右括号)
在此将再次看到灾难性的回溯。发生这种情况是因为(-?d+)*子表达式,其中包含d+子表达式。“*”或“+”量词应用于两个子表达式,并且同一字符串可以与它们中的每一个匹配,例如111。
将这些观察结果与前面检查的带有漏洞的正则表达式的条件进行比较:
(1)有两个子表达式,其中一个包含另一个子表达式。以下量词之一应用于它们中的每一个:“*”、“+”、“*?”、“+?”、{…}”。(-?d+)*)子表达式包含d+;
(2)有一个字符串可以与两个子表达式匹配。例如,1111字符串可以同时适合d+模板和(-?d+)*)。
newDate((-?d+)*)regex在实际项目RestSharp库中造成了一个漏洞(CVE-2021-27293)。
作为最后一个例子,在一个更复杂的正则表达式中寻找漏洞:
^(([A-Z]:|\main)(\[^\]+)*(,s)?)+$
这个表达式的任务是查找表示文件或目录路径列表的字符串。这列表中的每个元素之间用逗号和空格字符分隔。列表项可以由对应于以下两种类型之一的路径表示:
(1)完整路径,例如:D:catalogsubcatalogfile.txt。
(2)主文件夹的相对路径,例如:maincatalogfile.exe。
因此,对应于模式的字符串可能是这样的:
D:catalog, C:catalogfile.cs, mainfile.txt, main, projectmain.csproj
正则表达式将计算这样的字符串而不会出现任何问题。
这同样适用于几乎所有不正确的字符串处理,例如:
D:catalogfile.cscatalogfile.cscatalogfile.cscatalogfile.cscatalogfile.cscatalogfile.cs\
然而,如果将以下类型的字符串传递给正则表达式,情况就会改变:
D:mainmainmainmainmainmainmainmainmainmainmainmainmainmainmain\
图5正则表达式在处理 D:main ...main\ format
检查一下原始正则表达式(^(([A-Z]:|\main)(\[^\]+)*(,s)?)+$)详细信息。需要注意,相互跟随的子表达式([A-Z]:|\main)和(\[^\]+)*可以与同一个main字符串匹配。此外,以下子表达式((,s)?)可以忽略,因为`?'量词允许不与该模板匹配。
因此,可以简化原始正则表达式,只检查一种特殊情况——D:main ...main format:
^(([A-Z]:|\main)(\main)*)+$
当查看这个字符串的简化版本时,灾难性的回溯漏洞变得很明显。
(1)有一个带有“+”量词的子表达式(([a-z]:|\main)(\main)*)+。这个子表达式包含带有“*”量词的(\main)*。
(2)两个子表达式:(([A-Z]:|\main)(\main)*)+和(\main)* 可以匹配相同的字符串,例如,mainmainmain。
因此,脆弱表达式的两个条件都满足。
在此强调一下在^(([A-Z]:|\main)(\[^\]+)*(,s)?)+$正则表达式中导致灾难性回溯的主要因素:
如果至少缺少其中的一个,则正则表达式绝对安全。
以下了解保护正则表达式避免灾难性回溯的主要方法。将使用newDate((-?d+)*)作为例子。以下代码是用C#编写的。然而,类似的功能可能存在于其他支持正则表达式的编程语言中。
添加正则表达式处理字符串的执行时间限制。在.NET中,可以在调用静态方法或初始化新的正则表达式对象时设置matchTimeout参数。
C#
RegexOptions options = RegexOptions.None;
TimeSpan timeout = TimeSpan.FromSeconds(1);
Regex pattern = new Regex(@"newDate((-?d+)*)", options, timeout);
Regex.Match(str, @"newDate((-?d+)*)", options, timeout);
图6正则表达式的执行时间被限制为1秒
使用原子组(?>…):
C#
Regex pattern = new Regex(@"newDate((-?d+)*)", options, timeout);
对于标记为原子组的表达式,将禁用回溯功能。因此,在所有可能的匹配选项中,一个原子组总是只匹配一个包含最大字符数的子字符串。
尽管原子组是防止灾难性回溯的可靠方法,但建议谨慎使用它们。在某些情况下,使用原子组会降低正则表达式计算的准确性。
图7标记为原子组的子表达式不再容易受到灾难性回溯的影响
重写正则表达式,用安全的等价子表达式替换不安全的子表达式。例如,要查找newDate(13-09-2022)类型的字符串,可以使用newDate((d{2}-d{2{-d{4})),而不是newDate ((-?d+)*)。
后者有两个子表达式:(-?d+)*和d+。d+子表达式包含在(-?d+)*中。同一子字符串可以匹配这两个子表达式。安全的等效函数允许只与一个模板匹配任何子字符串,因为必须检查模板d{…}之间的'-'字符。
以下进行总结:
(1)正则表达式可能容易受到ReDoS攻击,其目的是停止或减慢应用程序。
(2)由于灾难性的回溯,应用程序变慢。如果有大量用于将输入字符串与正则表达式匹配的选项,并且其中没有正确的选项,则会发生这种情况。
(3)如果正则表达式包含至少一个易受攻击的子表达式,可能导致大量匹配选项,则正则表达式很容易发生灾难性的回溯。
(4)通过检查正则表达式中的以下条件,可以识别该表达式中的漏洞:
a.有两个子表达式,其中一个包含另一个子表达式。以下量词之一应用于它们中的每一个:“*”、“+”、“*?”、“+?”、{...}';
b.有一个字符串可以同时满足这两个子表达式。
原文标题:Catastrophic Backtracking: How Can a Regular Expression Cause a ReDoS Vulnerability?,作者:Andrey Moskalev