Node.js 10 月安全版本:10 个 CVE、4 个 high,先查 dns.resolve 与 TLS 的 IP 校验
2026-10-21 的安全版本已发布,覆盖 26.x、24.x、22.x 三条线:
| 版本线 | 安全版本 |
|---|---|
| 26.x | v26.12.1 |
| 24.x | v24.22.1 |
| 22.x | v22.23.4 |
同时更新了一批带公开漏洞的依赖:c-ares(26.x / 24.x / 22.x)、ca-cert(eMudhra Intermediate CA Compromise,26.x / 24.x / 22.x)、OpenSSL(24.x / 22.x)、SQLite(22.x)、Undici(26.x / 24.x / 22.x)。如果服务里直接依赖 undici,npm ls undici 之后需要对应升级。
下面按严重度记录 10 个 CVE 的触发条件、受影响版本和自查方式。
high
CVE-2026-105482:dns.resolve 截断含 NUL 的主机名
dns.resolve*() 和 dns.reverse() 会把主机名当作 C 字符串传给 c-ares,含 NUL 字节的名字被静默截断。attacker.example\0.trusted.example 实际查询的是 attacker.example,并正常返回其记录,不报错。
这是 CVE-2026-48930 的不完整修复——当时只覆盖了 dns.lookup() 和 net/tls 的 host 选项。危险模式很明确:应用先校验主机名(例如对域名 allowlist),再调用 dns.resolve*(),此时校验的名字和实际解析的名字不是同一个。
影响 26.x、24.x、22.x。自查:
node -p "require('dns').resolve('attacker.example\u0000.trusted.example','A',(e,a)=>console.log(e&&e.code,a))"
修复版本上这个调用应报错;旧版本会返回 attacker.example 的记录。代码层面同时搜校验与解析的组合:
grep -rn "dns\.\(resolve\|reverse\)" --include=*.js --include=*.ts .
CVE-2026-105483:全角数字写法的 IP 绕过 TLS 主机名校验
当 host 用非 ASCII 字符书写时(如全角数字 127.0.0.1),tls.checkServerIdentity() 不认为它是 IP 地址,转而拿它去匹配证书的 DNS 名和 CN。该 host 实际会归一化并解析到普通 IPv4 地址,于是 DNS:127.0.0.1 或 DNS:*.0.0.1 这类证书可以为它并未签发的 IP 端点通过认证。
这是 CVE-2026-48618 修复后引入的回归,影响 26.x 和 24.x。自查:
node -p "require('tls').checkServerIdentity('127.0.0.1', {subjectaltname:'DNS:127.0.0.1'})"
旧版本返回 undefined(校验通过),修复后应返回错误。
CVE-2026-106069:host 松散解析到 IP 时校验了另一个地址
host 是带前导零的非规范 IPv4 字面量(如 0127.0.0.1)时,tls.checkServerIdentity() 按 WHATWG URL 规则归一化,把前导零八进制读成 87.0.0.1;而实际连接走操作系统解析器,在 macOS 等平台上按十进制读成 127.0.0.1。结果是证书针对的 IP 和客户端连上的 IP 不一致,位于所连地址的服务器可以用另一 IP 的证书通过认证。
影响 26.x、24.x、22.x。自查:
node -p "require('tls').checkServerIdentity('0127.0.0.1', {subjectaltname:'IP Address:127.0.0.1'})"
CVE-2026-111497:Brotli 解码器越界写可致 RCE
Node.js 内置的 Brotli 解码器在非法 block switch 时,把它当成“需要更多输入”的临时状态而不是永久格式错误,流式解码器于是持续把输入字节追加到 8 字节的内部缓冲区,最终写越界。能向应用投递 Brotli 数据、并由 node:zlib 解压的攻击者可破坏进程内存,导致崩溃、内存泄露或远程代码执行。
影响 26.x、24.x、22.x。凡是对不可信来源做 Brotli 解压(HTTP Content-Encoding: br、上传包、消息体)的服务都算暴露面。
medium
CVE-2026-105481:keep-alive socket 接受了请求发送前就到达的响应
HTTP Agent 复用连接时,客户端可能把在请求发出之前就已到达的响应当作本次请求的合法响应。这是 CVE-2026-48931 的不完整修复:销毁异常空闲 keep-alive socket 的守卫只在事件循环轮询 I/O 时执行。如果事件循环正忙于同步任务,非请求响应已进入 socket 但尚未被读取,socket 被交给新请求时守卫被移除却没看到数据,这段数据就被解析为新请求的响应。
影响 26.x、24.x、22.x。
CVE-2026-106072:非规范 IPv4 让只有 DNS SAN 的证书认证 IP 端点
客户端用 0177.0.0.1、0x7f.0.0.1、2130706433、127.1 这类非规范写法连接时,tls.checkServerIdentity() 没有把它们识别为 IP 地址,而是归一化后拿结果去匹配证书 DNS 名。含 DNS:127.0.0.1 但没有 IP Address:127.0.0.1 SAN 的证书因此被接受,而同一张证书对 127.0.0.1 会被正确拒绝。
利用前提是:证书带数字形式的 DNS SAN,由客户端信任的 CA 签发,且平台解析器接受这些写法(如 glibc)。影响 26.x、24.x、22.x。自查:
for h in 0177.0.0.1 0x7f.0.0.1 2130706433 127.1; do
node -p "require('tls').checkServerIdentity('$h', {subjectaltname:'DNS:127.0.0.1'})"
done
CVE-2026-106074:crypto 熵缓存写进快照与 SEA,进程间重复取值
crypto.randomUUID()、crypto.randomUUIDv7()、crypto.randomInt() 使用的内部熵缓存会被保存进 user-land 启动快照(--build-snapshot)和以 useSnapshot 构建的单文件应用。如果在构建快照时调用过这些函数,或调用过依赖它们的 API(如 URL.createObjectURL()),那么从该快照启动的每个进程在缓存重新填充前都会返回相同的值:最多 127 个 UUID、约 1000 个 randomInt() 值。
后果是跨进程的唯一标识冲突,以及通过观察一个进程预测另一个进程发出的值。影响 26.x、24.x、22.x。自查构建脚本与启动参数:
grep -rn "build-snapshot\|useSnapshot" .
low
CVE-2026-106070:ZipFile 头校验失败后返回攻击者控制的前缀字节
在 local header 与 central directory 不匹配的归档上读取 zipEntry.modified(直接读或在压缩归档过程中读),校验错误被捕获,但解析出的 local header 被缓存且没有内容偏移。后续读取内容时返回从文件开头起、由攻击者控制的字节,而不是拒绝该归档;由于 central directory 的 CRC 也在攻击者手里,CRC-32 校验仍会通过。基于 Node.js 的扫描器可能因此放行与其它 ZIP 实现解压结果不同的内容。影响 26.x。
CVE-2026-106071:Permission Model 通配符 radix 查找过宽
请求路径比通配符授权的公共前缀更短时,radix 树查找提前停止比较并判定为匹配。例如 --allow-fs-read=/etc/passwd.* 也会放行读取 /etc/passwd。触发条件是通配符授权直接位于权限树根部,比如它是该作用域下唯一的授权。与 CVE-2026-58043 的根因不同。影响 26.x。
CVE-2026-106073:fs.realpathSync 未做读权限检查
fs.realpathSync() 缺少必要的读权限检查。这是 CVE-2026-21715 的不完整修复,当时只覆盖了 fs.realpathSync.native()。在 --permission 下、--allow-fs-read 受限时,代码仍可用 fs.realpathSync() 探测文件是否存在、解析符号链接目标、枚举允许目录之外的路径。影响 26.x、24.x、22.x。
26.13.0 'Lithium'(2026-10-28):只是转入 LTS
26.13.0 标志着 26.x 进入 Long Term Support,代号 'Lithium',支持到 2029 年 4 月底。除了更新 process.release 这类用于表明 LTS 身份的元数据,相对 26.12.1 没有其它改动。
从更早的 LTS(Node.js 24、22 及更早)升级时注意:自 Node.js 25 起,Linux 官方二进制链接了 libatomic,部分发行版需要单独安装。
升级与自查清单
# 1. 确认当前版本
node -v
# 2. 直接依赖的 undici 版本
npm ls undici
# 3. 校验修复是否生效(应报错/返回错误对象)
node -p "require('dns').resolve('attacker.example\u0000.trusted.example','A',(e,a)=>console.log(e&&e.code,a))"
node -p "require('tls').checkServerIdentity('127.0.0.1', {subjectaltname:'DNS:127.0.0.1'})"
node -p "require('tls').checkServerIdentity('0127.0.0.1', {subjectaltname:'IP Address:127.0.0.1'})"
# 4. 排查“先校验后解析”的代码路径
grep -rn "dns\.\(resolve\|reverse\)" --include=*.js --include=*.ts .
# 5. 排查快照 / SEA 中的 crypto 调用
grep -rn "build-snapshot\|useSnapshot" .
部署侧把镜像 tag 固定到 26.12.1 / 26.13.0(24 线 24.22.1、22 线 22.23.4),不要用浮动 tag 等下一次拉取;Permission Model 用户还要回看 --allow-fs-read / --allow-fs-write 里带 .* 的授权是否落在权限树根部。
下载:Node.js v22.23.4、v24.22.1、v26.12.1。
Sources: 、