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)}/aOWASP 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:

  1. 避免复杂的正则表达式: 尽量避免使用容易引发回溯的模式,如嵌套的量词、重复的选择分支等。
  2. 使用时间限制: 在执行正则表达式匹配时,设置匹配超时时间,防止长时间的计算。
  3. 输入验证和过滤: 对用户输入进行严格的验证和过滤,以减少不可信输入的影响。
  4. 正则表达式库升级: 使用更新、更安全的正则表达式库或引擎,有些现代的库对 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:之前匹配的文本。
  • 逻辑:

    1. 判断条件:
      • 如果 prevText 为空,或者包含了 delimiter,函数会返回一个简单的正则表达式:[^delimiter]+?,表示匹配除 delimiter 之外的任意字符,并且是惰性匹配(尽可能少的匹配)。
    2. 更复杂的情况:
      • 如果 prevText 不为空且不包含 delimiter,函数会返回一个更复杂的正则表达式:
        • 首先尝试匹配 prevText
        • 如果匹配不上 prevText,则尝试匹配不包含 delimiter 的字符序列,并确保这些字符序列不会匹配 prevText,这是通过一个负向前瞻断言 (?!prevText) 来实现的。
  • escapeString 函数:

    • 这个函数用来对 delimiterprevText 进行转义,以确保它们在正则表达式中被正确处理为字面值,而不是被解析为特殊字符。