
最后确认日期:2026-11-04 (KST)。各项数值仅记录了下文 References 来源中确认的值,若来源之间数值不同,则全部并列标注。
c/dec/decode.c)忽略了错误块切换(block switch)的错误返回值,在损坏状态下继续解码,导致在缓冲区边界外写入(越界写入)。Brotli/brotlicffi、Bun、.NET、Envoy、Istio 等。必须存在攻击者控制的 Content-Encoding: br 数据的解压路径才会受到影响。| 项目 | 内容 |
|---|---|
| CVE ID | CVE-2026-111497 (GHSA-9v8h-qqgm-4m35) |
| 别名 | Node2Shell(Imperva 命名)、HTTP2Shell、Broccoli2Shell。与名称不同,并非 HTTP/2 协议缺陷 |
| 公开日期 | 2026-10-21 Node.js 安全发布及 Brotli 补丁发布 / 2026-10-22 CVE 记录公开 |
| 发现者 | OpenAI Codex Security (Khai Tran, Sylvie Mayer) |
| 影响产品/版本 | Google Brotli(libbrotlidec) 1.0.3 ~ 1.2.0 以及捆绑/链接该库的产品(见下表) |
| 补丁版本 | Brotli 1.0.10 / 1.1.1 / 1.2.1、Node.js 22.23.4 / 24.22.1 / 26.12.1、Python Brotli 1.2.1、brotlicffi 1.2.1.0、Bun 1.4.4 |
| CVSS v4.0 (CNA: Google) | 9.2 Critical — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |
| CVSS v3.1 (NVD) | 9.8 Critical — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CVSS v3.1 (Red Hat / Amazon Linux) | 8.1 — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H(反映攻击要求 AT:P 及平台加固) |
| 厂商自身评级 | Node.js: High / Red Hat: Important |
| CWE | CWE-787 越界写入(NVD、Red Hat、加拿大网络中心) / CWE-252 未检查返回值(CISA KEV 描述,部分分析文章作为根本原因并列标注) |
| 实际利用情况 | 已确认利用。 CISA KEV 于 2026-10-27 列入,联邦机构行动截止日期 2026-11-10。蜜罐运营商(Previdian、ELLIO)于 10-26~27 观测到利用尝试 |
| 组件 | 影响版本 | 修复版本 |
|---|---|---|
| Google Brotli (C, libbrotlidec) | 1.0.3 ~ 1.2.0 | 1.0.10, 1.1.1, 1.2.1 |
| Node.js 26.x (Current) | 低于 26.12.1 | 26.12.1 |
| Node.js 24.x (LTS) | 低于 24.22.1 | 24.22.1 |
| Node.js 22.x (LTS) | 低于 22.23.4 | 22.23.4 |
| Node.js 25.x / 23.x / 20.x 及以下 | 所有版本 (EOL) | 无补丁 → 迁移至 22/24/26 |
| Bun | 1.4.3 及以下 | 1.4.4 |
Python Brotli (PyPI) | 1.2.0 及以下 | 1.2.1 |
Python brotlicffi | 低于 1.2.1.0 | 1.2.1.0 |
| .NET / PowerShell | 参考厂商建议 | Microsoft 紧急 .NET 更新 (dotnet/announcements#494) |
| Envoy / Istio | 参考厂商建议 | 10 月初发布修复版本(未明确说明缺陷) |
| VMware SDDC Manager (VCF / vSphere Foundation) | 参考 Broadcom 建议 | 参考 Broadcom 建议 |
间接暴露路径:Python requests、urllib3、aiohttp[speedups]、httpx 在安装了 Brotli/brotlicffi 时会自动解压 br 响应。curl 在使用解压选项时也会受影响。
| 日期 | 事件 |
|---|---|
| 02-27 | 向 google/brotli 仓库提交修复提交(标记为安全议题,未发布) |
| 10月初 | 发布 Envoy / Istio 修复版本 |
| 10-21 | Node.js 安全版本发布(22.23.4 / 24.22.1 / 26.12.1),Google Brotli 1.0.10 / 1.1.1 / 1.2.1 发布 |
| 10-22 | CVE 记录公开,Imperva 在 Node.js 24.22.0(Linux arm64)上演示了代码执行 |
| 10-23 | 发布已修补的 PyPI 包 |
| 10-25 | Microsoft .NET 公告及更新 |
| 10-26 | ELLIO 报告利用流量 |
| 10-27 | 列入 CISA KEV(处置期限 11-10) |
⚠️ 在10月24日至28日之间撰写的一些分析文章称“未确认实际攻击、未列入 KEV”,但这是以 KEV 列入之前的时点为准,与当前情况不同。
BROTLI_FALSE 返回值Brotli 解码是一个状态机(state machine)。流由元块(metablock)单元构成,在每个元块内部会出现用于切换字面量(literal)、插入-复制命令(insert-and-copy command)和距离(distance)的**块类型(block switch)**的指令。
DecodeBlockTypeAndLength() 解析块切换并返回 BROTLI_BOOL。问题在于 BROTLI_FALSE 被用于两种完全不同的含义。
| 含义 | 性质 | 正确处理 |
|---|---|---|
| 需要更多输入(needs more input) | 可恢复 | 等待下一次输入后恢复 |
| 元块中只有1个块类型,切换本身不可能 | 致命格式错误 | 拒绝该流 |
调用方未检查该返回值。
DecodeCommandBlockSwitch)被声明为 static void,不保留返回值,BROTLI_SAFE 宏的 non-safe 分支也只是调用函数并丢弃结果。(SonicWall 分析)DecodeLiteralBlockSwitch)中返回值被完全丢弃。各分析所指出的函数不同,但根据发行版补丁说明(TuxCare),字面量、命令、距离三种块切换解码器全部都被修改为返回错误码,因此可以认为三条路径均相关。结果,即使发生格式错误,解码器也会像切换成功一样继续推进,块长度计数器保持为0,位置(position)·距离状态被带入本应被拒绝的路径。
BrotliDecoderState)的位置·距离字段。ProcessCommandsInternal() 的反向引用复制(backward-reference copy)以 copy_dst = &s->ringbuffer[pos] 计算目标,并通过 memmove16 执行 16字节向量存储。由于 pos 不再被限制在环形缓冲区范围内,写入发生在分配区域之外。pos,因此这成为 write-what-where 原语(primitive)。以下是根据公开分析(SonicWall、Redfox、TuxCare 补丁说明)的叙述重构变更要点的概念代码。并非实际提交的原文代码,条件表达式等细节为推测。准确的 diff 请从 google/brotli v1.2.1 发布及 Node.js
deps/brotli变更部分确认。(本文档撰写时未能将修复提交哈希作为一手来源确认。)
Before(脆弱)
/* 형식 오류와 "입력 부족"이 같은 BROTLI_FALSE로 표현됨 */
static BROTLI_BOOL DecodeBlockTypeAndLength(int safe, BrotliDecoderState* s, int tree_type) {
...
if (/* 블록 타입이 1개뿐인 메타블록 */) {
return BROTLI_FALSE; /* 치명적 오류지만 복구 가능 신호와 구분 불가 */
}
...
return BROTLI_TRUE;
}
static void DecodeCommandBlockSwitch(BrotliDecoderState* s) {
DecodeBlockTypeAndLength(0, s, 1); /* 반환값 폐기 → 손상 상태로 계속 진행 */
}
之后(补丁)
/* 명시적 오류 코드 반환 */
static BrotliDecoderErrorCode DecodeBlockTypeAndLength(int safe, BrotliDecoderState* s, int tree_type) {
...
if (/* 블록 타입이 1개뿐인 메타블록 */) {
return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH); /* 신규 오류 코드 */
}
...
return BROTLI_DECODER_SUCCESS;
}
/* 리터럴/명령/거리 블록 전환 래퍼가 오류 코드를 전파하고,
ProcessCommandsInternal()은 신규 매크로 BROTLI_SAFE_WITH_STATUS로
결과를 확인해 비성공 시 디코딩을 즉시 중단 */
| 分类 | Before | After |
|---|---|---|
| 返回类型 | BROTLI_BOOL(语义重叠) | BrotliDecoderErrorCode(语义明确) |
| 错误块切换 | BROTLI_FALSE → 忽略 | 返回 BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH |
| 调用方 | void 包装器,丢弃结果 | 错误传播 + 通过 BROTLI_SAFE_WITH_STATUS 检查后中止 |
| 结果 | 以损坏状态继续解码 → OOB Write | 正常拒绝该流 |
修补后的 Node.js 会以 ERR__ERROR_FORMAT_BLOCK_SWITCH 错误拒绝此流。新常量在 Node.js 中通过 zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH 暴露,在修复构建中为 -17,在易受攻击的构建中为 undefined(Redfox)。
| 条件 | 内容 |
|---|---|
| 认证 | 不需要 — 解压缩在应用程序认证逻辑之前执行 |
| 网络位置 | 远程(AV:N)。目标必须具有解压攻击者控制的 Brotli 数据的路径 |
| 易受攻击版本 | 包含 Brotli 1.0.3 ~ 1.2.0 的运行时/库 |
| 流条件 | 块类型为 1 个的元块 + 错误的块切换 |
| 传递方式 | 需要分块(chunked)传递以使解码器因等待输入而暂停。一次性(one-shot)解码会一次性解压全部内容,无法分离损坏阶段 |
| 载荷大小 | 为耗尽块长度计数器,至少需要约 2MB,按实际编码计算需要约 22~23MB 级别的流(SonicWall,公开 PoC 实验室) |
暴露判定标准:纯 Node.js http 服务器即使接收到 br 正文也不会自动解压。必须由代码或中间件调用解码器。主要暴露路径如下。
| 方向 | 路径 | 备注 |
|---|---|---|
| 入站 | Express 5 / body-parser 2.x 的 json()、raw() 等默认 inflate 设置 | Imperva 在 Express 5.0.0br 正文 |
| 出站 | Node.js 内置 fetch()、Python requests/urllib3/aiohttp/httpx | Webhook、链接预览、URL 抓取功能等。WAF 无法看到的路径 |
| 直接调用 | zlib.brotliDecompress*、createBrotliDecompress、brotli.decompress 等 | 用于不可信输入的情况 |
路径 A — 入站请求正文
sequenceDiagram
autonumber
participant A as 공격자
participant P as 리버스 프록시/WAF
participant N as Node.js 앱 (Express 5)
participant D as Brotli 디코더 (취약)
A->>P: POST /endpoint<br/>Content-Encoding: br<br/>Transfer-Encoding: chunked
P->>N: 요청 전달 (본문 크기 제한 없을 때)
N->>D: body-parser가 인증 전 자동 inflate
Note over D: 1단계: 대용량 스트림으로 블록 길이 카운터 소진<br/>잘못된 블록 전환 → BROTLI_FALSE 무시
Note over D: 디코더가 손상 상태로 입력 대기(parked)
A->>N: 2단계: 작은 청크 추가 전송
N->>D: 이어서 디코딩
Note over D: 8바이트 버퍼 초과 쓰기 → pos/distance 위조<br/>memmove16이 ringbuffer+pos에 16바이트 저장
D-->>N: 메모리 손상 → 크래시 또는 코드 실행
路径 B — 出站响应(类 SSRF 模式)
sequenceDiagram
autonumber
participant A as 공격자
participant N as 대상 서비스 (fetch / requests)
participant S as 공격자 제어 서버
participant D as Brotli 디코더 (취약)
A->>N: "이 URL 미리보기 / 웹훅 등록" 요청
N->>S: GET (Accept-Encoding에 br 포함)
S-->>N: Content-Encoding: br 조작된 응답
N->>D: HTTP 클라이언트가 자동 해제
D-->>N: 메모리 손상 → 크래시 또는 코드 실행
Note over N,S: 아웃바운드 응답은 인바운드 WAF를 거치지 않음