path-to-regexp漏洞事件
path-to-regexp这个包这两天疯狂更新,原因是因为有某一种路由规则可能会导致ReDos攻击,导致很多基础应用也跟着疯狂更新。
这是Github上原始的漏洞详情:
Backtracking regular expressions cause ReDoS
Using /:a-:b
will produce the regular expression /^\/([^\/]+?)-([^\/]+?)\/?$/
. This can be exploited by a path such as /a${'-a'.repeat(8_000)}/a
. OWASP has a good example of why this occurs, but the TL;DR is the /a
at the end ensures this route would never match but due to naive backtracking it will still attempt every combination of the :a-:b
on the repeated 8,000 -a
.
使用
/:a-:b
将生成正则表达式/^\/([^\/]+?)-([^\/]+?)\/?$/
。这可通过诸如/a${'-a'.repeat(8_000)}/a
的路径加以利用。OWASP 有一个很好的示例来说明为何会发生这种情况,但 TL;DR 是末尾的/a
确保此路由永远不会匹配,但由于朴素回溯,它仍将尝试:a-:b
在重复的 8,000-a
上的每种组合。
ReDos是什么?
以下来源于ChatGPT-4o
ReDoS(Regular Expression Denial of Service)是正则表达式拒绝服务攻击的缩写。它是一种利用特定正则表达式的性能问题进行的拒绝服务攻击。当正则表达式处理恶意构造的输入时,会导致处理时间急剧增加,从而占用大量CPU资源,使得应用程序性能大幅下降或完全瘫痪。
ReDoS 的攻击通常利用正则表达式中的某些模式(如重复、嵌套的量词等),这些模式在匹配失败时会导致大量的回溯操作。例如,正则表达式 ^(a|aa)*$
在匹配类似 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
这样的字符串时,会导致大量的回溯和计算,从而使得正则表达式匹配变得非常慢。
如何防止 ReDoS:
- 避免复杂的正则表达式: 尽量避免使用容易引发回溯的模式,如嵌套的量词、重复的选择分支等。
- 使用时间限制: 在执行正则表达式匹配时,设置匹配超时时间,防止长时间的计算。
- 输入验证和过滤: 对用户输入进行严格的验证和过滤,以减少不可信输入的影响。
- 正则表达式库升级: 使用更新、更安全的正则表达式库或引擎,有些现代的库对 ReDoS 有更好的防护措施。
通过这些方法,可以有效降低 ReDoS 攻击对系统的影响。
path-to-regexp如何修复的
https://github.com/pillarjs/path-to-regexp/pull/320/files
以1.x的分支修复来看,主要增加这几个变更:
var prevText = prefix || (typeof tokens[tokens.length - 1] === 'string' ? tokens[tokens.length - 1] : '')
pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : restrictBacktrack(delimiter, prevText))
function restrictBacktrack(delimiter, prevText) {
if (!prevText || prevText.indexOf(delimiter) > -1) {
return '[^' + escapeString(delimiter) + ']+?'
}
return escapeString(prevText) + '|(?:(?!' + escapeString(prevText) + ')[^' + escapeString(delimiter) + '])+?'
}
之前则是
pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
})
这段代码的 restrictBacktrack
函数主要是为了生成一个正则表达式,用来限制正则匹配中的回溯操作,从而减少 ReDoS 攻击的风险。下面是对这段代码的详细解释:
-
输入参数:
delimiter
:一个作为分隔符的字符或字符串,用来界定匹配的边界。prevText
:之前匹配的文本。
-
逻辑:
- 判断条件:
- 如果
prevText
为空,或者包含了delimiter
,函数会返回一个简单的正则表达式:[^delimiter]+?
,表示匹配除delimiter
之外的任意字符,并且是惰性匹配(尽可能少的匹配)。
- 如果
- 更复杂的情况:
- 如果
prevText
不为空且不包含delimiter
,函数会返回一个更复杂的正则表达式:- 首先尝试匹配
prevText
。 - 如果匹配不上
prevText
,则尝试匹配不包含delimiter
的字符序列,并确保这些字符序列不会匹配prevText
,这是通过一个负向前瞻断言(?!prevText)
来实现的。
- 首先尝试匹配
- 如果
- 判断条件:
-
escapeString
函数:- 这个函数用来对
delimiter
和prevText
进行转义,以确保它们在正则表达式中被正确处理为字面值,而不是被解析为特殊字符。
- 这个函数用来对