Fedify库发现正则表达式拒绝服务(ReDoS)漏洞,HTML解析器可被攻击

本文详细披露了Fedify库中一个高严重性的正则表达式拒绝服务(ReDoS)漏洞。该漏洞存在于HTML解析的正则表达式中,攻击者可利用恶意构造的小型HTML响应,导致Node.js事件循环长时间阻塞,从而实现拒绝服务攻击。

Fedify 库在 HTML 解析正则表达式中存在 ReDoS 漏洞 · CVE-2025-68475

漏洞摘要 一个正则表达式拒绝服务(ReDoS)漏洞存在于 Fedify 的文档加载器中。位于 packages/fedify/src/runtime/docloader.ts:259 的 HTML 解析正则表达式包含嵌套的量词,当处理恶意构造的 HTML 响应时,会导致灾难性的回溯。

攻击者控制的联邦服务器可以通过一个约 170 字节的恶意 HTML 负载进行响应,使受害者的 Node.js 事件循环阻塞 14 秒以上,从而导致拒绝服务。

受影响的版本

  • npm 包: @fedify/fedify
  • 受影响版本:
    • < 1.6.13
    • >= 1.7.0, < 1.7.14
    • >= 1.8.0, < 1.8.15
    • >= 1.9.0, < 1.9.2
  • 已修补版本:
    • 1.6.13
    • 1.7.14
    • 1.8.15
    • 1.9.2

漏洞详情

易受攻击的代码 漏洞位于 packages/fedify/src/runtime/docloader.ts 的第 258-264 行:

1
2
3
4
5
6
7
8
// 第 258-259 行:包含嵌套量词的易受攻击的正则表达式
const p = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig;

// 第 261 行:响应体没有大小限制
const html = await response.text();

// 第 264 行:正则表达式执行循环
while ((m = p.exec(html)) !== null) rawAttribs.push(m[2]);

根本原因分析 该正则表达式具有带交替的嵌套量词,这是一个典型的 ReDoS 模式:

1
2
3
4
5
/<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig
                                                        ^^
                                                   外部量词 (+)
           ^^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                     具有交替的内部模式
  • 外部量词: ((\s+...)+) - 一个或多个属性组
  • 内部交替: ("[^"]*"|'[^']*'|[^\s>]+) - 匹配属性值的多种方式

当正则表达式无法匹配(例如,一个不完整的 HTML 标签)时,正则表达式引擎会通过嵌套模式所有可能的匹配方式进行指数级回溯。

攻击向量

  1. 受害者的 Fedify 应用程序调用 lookupObject("https://attacker.com/@user") 来获取角色配置文件。
  2. 攻击者的服务器以 Content-Type: text/html 进行响应。
  3. 代码路径: lookupObject()documentLoader()getRemoteDocument() → HTML 解析(第 258-287 行)。
  4. 第 261 行: response.text() 无大小限制地读取整个正文。
  5. 第 264 行: 正则表达式执行触发灾难性回溯。
  6. 事件循环被阻塞数秒至数分钟,导致 DoS。

为何可利用

  • 无响应大小限制:通过 response.text() 完全读取 HTML 正文,没有进行 Content-Length 验证。
  • 默认无超时:AbortSignal 是可选的且未强制执行。
  • 远程利用:攻击者只需要受害者从其 URL 获取内容。
  • 无需身份验证:联邦通常涉及从不信任的服务器获取配置文件。
  • 可放大:多个并发请求可以完全禁用服务。

概念验证 提供了独立的 Node.js 脚本来验证漏洞,并展示了指数级的时间复杂度。示例输出显示,重复次数每增加 2 次,处理时间大约变为原来的 4 倍(O(2^n) 复杂度):

1
2
3
4
5
6
7
8
Repetitions | Payload Size | Time
------------|--------------|--------
18          | 111 bytes    | 14ms
20          | 123 bytes    | 51ms
22          | 135 bytes    | 224ms
24          | 147 bytes    | 852ms
26          | 159 bytes    | 3.26s
28          | 171 bytes    | 14.10s

还提供了一个完整的基于 Docker 的 PoC 环境,用于在隔离环境中演示。

影响 受影响对象

  • 所有使用 lookupObject()getDocumentLoader() 或内置文档加载器从外部 URL 获取内容的 Fedify 应用程序。
  • 任何从潜在不受信任的来源获取角色配置文件、帖子或其他 ActivityPub 对象的联邦服务器。
  • 遵循标准联邦模式的服务器 - 获取远程角色是正常操作。

严重性评估

因素 评估
攻击向量 网络(远程)
攻击复杂性 低(简单的负载)
所需权限
用户交互
影响 可用性 (DoS)
范围 服务范围

实际场景

  1. 一个由 Fedify 驱动的 Mastodon 兼容服务器收到来自 @attacker@evil.com 的关注请求或提及。
  2. 服务器尝试通过 lookupObject() 获取攻击者的配置文件。
  3. 攻击者的服务器用恶意 HTML 进行响应。
  4. 受害服务器的事件循环被阻塞 14 秒以上。
  5. 在此期间,所有其他请求都会被排队并可能超时。
  6. 重复攻击可导致持续的服务不可用。

建议的修复方案 方案 1:使用合适的 HTML 解析器(推荐) 用不会受到回溯问题影响的 DOM 解析器替换基于正则表达式的 HTML 解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 linkedom(轻量级 DOM 实现)
import { parseHTML } from 'linkedom';

// 用以下代码替换第 258-287 行:
const { document } = parseHTML(html);
const links = document.querySelectorAll('a[rel="alternate"], link[rel="alternate"]');

for (const link of links) {
  const type = link.getAttribute('type');
  const href = link.getAttribute('href');
  
  if (
    href &&
    (type === 'application/activity+json' ||
     type === 'application/ld+json' ||
     type?.startsWith('application/ld+json;'))
  ) {
    const altUri = new URL(href, docUrl);
    if (altUri.href !== docUrl.href) {
      return await fetch(altUri.href);
    }
  }
}

方案 2:添加响应大小限制 如果必须使用正则表达式,至少应添加大小限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
const contentLength = parseInt(response.headers.get('content-length') || '0');

if (contentLength > MAX_HTML_SIZE) {
  throw new FetchError(url, 'Response too large');
}

const html = await response.text();
if (html.length > MAX_HTML_SIZE) {
  throw new FetchError(url, 'Response too large');
}

方案 3:重构正则表达式 如果倾向于使用正则表达式方法,可使用原子分组或占有量词(如果支持),或重构以避免嵌套量词:

1
2
3
// 使用非回溯方法进行显式属性匹配
const tagPattern = /<(a|link)\s+([^>]+)>/ig;
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|(\S+))/ig;

资源

  • OWASP: 正则表达式拒绝服务 (ReDoS)
  • CWE-1333: 低效的正则表达式复杂性
  • Cloudflare 宕机分析 (ReDoS 示例)

参考

安全评分

  • CVSS 总体评分: 7.5(高)
  • CVSS v3 向量: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
  • 弱点: CWE-1333 低效的正则表达式复杂性
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计