如果本地执行 node -v 已显示新版本,而生产服务器、CI Runner 或容器仍使用旧运行时,这次升级就不能算完成。处理顺序是:先按 Node.js 官方安全公告确认每个受影响分支的安全版本,再分别记录开发机、CI、镜像和实际运行进程中的 node -v 与 process.execPath;eMudhra 证书事件另行检查服务器证书链和客户端信任源。
热点文章以“10 个漏洞、8 枚危险证书”为题,但现有材料没有给出完整十项漏洞清单,而且分支与版本存在冲突:提到 6.x、22.x、24.x 和“10 余个漏洞”,则列出 22.x、24.x、26.x 及一组候选修复版本。发布和生产变更时不能直接采用这些二手信息,必须回到 Node.js 官方漏洞公告逐项核对。
分开处理运行时漏洞与证书问题
这里说的八张证书,准确口径是八张 eMudhra 中间 CA 证书,不是八张根证书。由于相关中间 CA 的私钥生成随机性不足,攻击者一旦掌握相应私钥,就可能签发原本会被信任的证书,因此相关浏览器厂商和开源软件项目开始移除或阻断这些证书。
这条证书信任链与 Node.js 运行时漏洞不是同一项修复。升级 Node.js 不会自动替换 Web 服务仍在发送的证书链,也不会让浏览器、操作系统、反向代理和容器镜像中的信任库同时更新。反过来,只更新客户端信任库也不能修补存在路径遍历或远程代码执行风险的 Node.js 组件及依赖。
如果服务器仍在发送由受影响中间 CA 签发的证书链,应从证书提供方取得新证书并替换服务器端链路;如果服务器链已经正常,而某类客户端仍接受旧链,再检查该客户端使用的信任源。浏览器能打开网页、Node.js 客户端却连接失败,通常就说明两套信任源并不相同。
按官方公告核对受影响分支和安全版本
在填写版本表之前,先记录当前冲突:给出的候选版本是 22.23.4、24.22.1、26.12.1 或更高版本,但 提到的受影响分支又包含 6.x。现有资料没有说明 6.x 对应的修复版本,也没有提供完整 CVE 清单,因此这些数字只能算二手报道中的候选值,不能直接成为生产白名单。
Node.js 官方漏洞查询入口是。发布前应按 CVE、组件、受影响分支、严重级别和修复版本逐项抄录,并把本次具体公告 URL 附在表格中。若某个组件只在新闻里被描述为“TLS、DNS 或权限问题”,却没有 CVE、受影响范围或安全版本,就应继续核验,不能根据描述自行补造。
| CVE | 组件或路径 | 受影响版本 | 严重级别 | 安全版本 | 官方来源 |
|---|---|---|---|---|---|
| CVE-2026-111497 | Brotli 解码器越界写入,可能导致内存损坏和远程代码执行 | 现有资料未列完整范围 | 报道只称本次更新为最高严重级别,不能当作该 CVE 的单独评分 | 待从具体公告抄录 | Node.js 官方漏洞公告 |
| 待补 | TLS 证书验证绕过 | 待核对 | 待核对 | 待核对 | 发布前附具体公告 URL |
| 待补 | DNS 解析异常行为 | 待核对 | 待核对 | 待核对 | 发布前附具体公告 URL |
| 待补 | 权限模型绕过 | 待核对 | 待核对 | 待核对 | 发布前附具体公告 URL |
这张表是发布闸门,不是完整的十项漏洞清单。正式发布时如果仍缺 CVE、受影响范围或安全版本,就应继续使用“多项漏洞”而不是给出看似完整的十项列表。
也不要把不同维护分支简单写成一个全局的 >= 条件。各分支的补丁回移方式可能不同,升级时必须先确认当前运行的是哪个大版本,再采用该分支官方列出的修复版本。安全公告没有覆盖的旧分支,不能靠自行改包名或覆盖安装解决。
核对实际运行的 Node.js 副本
node -v 只能证明当前命令解析到的那一个 Node.js。同一台机器可能同时存在系统包、版本管理器安装目录、项目私有运行时和容器内二进制;CI 还会受到 Runner 镜像、工具缓存和步骤安装顺序影响。进程管理器如果使用绝对路径启动 Node.js,登录 shell 中的 node 甚至未必代表生产进程实际使用的副本。
Linux/macOS 的 bash、zsh 可以先执行:
type -a node
node -v
node -p "process.execPath"
node -p "process.versions"
type -a node 用来发现 PATH 中的重复安装,process.execPath 才是这个 Node.js 进程实际使用的可执行文件。process.versions 还能记录 OpenSSL、模块 ABI 等构建信息,跨大版本升级时应把它和版本号一起留档。
Windows PowerShell 可以执行:
Get-Command node -All
node -v
node -p "process.execPath"
node -p "process.versions"
这些命令的输出不应预设为一套统一的“标准输出”;不同机器的路径和版本本来就应不同。如果各处结果不一致,就分别标记为开发机、CI、基础镜像、业务镜像、虚拟机或生产实例,不要用最高版本覆盖所有记录。
对已经启动的服务,还应进入对应的容器、Pod 或虚拟机执行检查。仅在宿主机运行 node -v,无法证明容器内进程已经升级;仅运行一次临时容器,也只能证明该镜像候选版本存在,不能证明生产中的旧容器已经重启。
按部署方式升级并验证
开发机应按原来安装 Node.js 的入口,将 Node.js 升级到官方确认的安全版本;这些入口包括系统包、版本管理器或官方安装方式。不要为了这次事件临时混用另一套安装机制,否则 PATH 和进程管理器可能继续指向旧副本。
CI 需要同时更新 Runner 镜像、项目工具链配置和可能缓存的 Node.js 安装。每条安全发布或部署任务开始时都应记录 node -v 与 process.execPath;否则,即使日志中出现新版本,也不能证明真正执行测试或构建的步骤没有使用缓存副本。
Docker 应更新基础镜像中的 Node.js,重新构建业务镜像,再验证待部署镜像本身:
docker run --rm --entrypoint node "$IMAGE_NAME" -v
docker run --rm --entrypoint node "$IMAGE_NAME" -p "process.execPath"
$IMAGE_NAME 应替换为实际候选镜像,最好使用不可变标签或镜像摘要。不要在已经生成的容器内部修改 Node.js,再把容器当成可复现部署结果;下次重建时这些修改仍会消失。
服务器上的 Node.js 应按照当前发行版、托管平台或进程管理器提供的方式升级,然后重启所有工作进程。在确认安装来源,以及实际采用 systemd、容器编排还是进程管理器之前,不宜给出一套通用安装命令。
如果从当前维护分支跨到另一个大版本,还要检查原生模块、构建工具、运行时 API 和权限行为。安全补丁升级不能默认视为无副作用的覆盖安装;依赖不支持目标版本时,应先按项目与依赖的官方升级说明处理。
旧版本回滚也不是解决方案,因为回滚会重新暴露已经修补的漏洞。若新版本带来兼容性问题,应根据风险范围限制流量、修复应用或等待补丁,而不是把整个生产集群退回受影响版本。
单独检查旧依赖
运行时升级不会删除项目依赖。现有材料提到,一个广泛使用的解压包存在路径遍历和符号链接链问题,恶意压缩包可能在解压时覆盖启动脚本或配置,并进一步导致远程代码执行;由于参考资料没有给出包名,这里不能自行补写。
npm 项目可以先查看顶层依赖和完整依赖树:
npm ls --depth=0
npm ls --all
拿到官方公告中的准确包名后,再沿依赖树确认它由哪个直接依赖引入,并检查 package.json、package-lock.json、pnpm-lock.yaml、yarn.lock 以及实际安装的 node_modules。锁文件没有直接出现该包,不代表运行时一定安全;它可能位于某个依赖的传递分支中。
安装脚本也要单独检查。参考资料记录过 npm 供应链投毒事件:恶意包会通过安装脚本自动执行代码,并尝试窃取 Token、云服务密钥、SSH 私钥和数据库连接信息。 这与本次 Node.js 运行时漏洞不是同一个原因,但排查结果不能混为一谈。发现来源不明的包、异常 preinstall、postinstall 或执行痕迹时,应先停止相关构建和运行,再按安全事件流程隔离环境、检查持久化残留并轮换可能暴露的凭据。
分别回归 Brotli、TLS、DNS 和权限模型
现有资料能明确确认 Brotli 漏洞编号为 CVE-2026-111497,并提到同一轮更新涉及 TLS 证书验证、DNS 解析和权限模型问题,但没有提供后三项的完整编号与版本范围。 因此验收不能只检查页面能打开,而要分别覆盖四条正常业务路径。
| 路径 | 正常回归 | 必要的反例 | 应保留的证据 |
|---|---|---|---|
| Brotli | 请求受控接口返回的 Brotli 内容,确认状态码、响应头和解压后的正文均正常 | 使用官方提供的安全测试样本;在隔离环境中确认畸形输入返回受控错误且进程不退出 | Node.js 版本、实际路径、请求地址、正文校验结果 |
| TLS | 使用合法公网证书建立连接,并确认目标主机名与证书匹配 | 受控测试环境中的不受信任、过期或主机名错误证书必须失败 | 证书链、信任来源、Node.js 错误码和退出状态 |
| DNS | 对业务使用的域名执行应用实际采用的解析 API,结果应符合该域名的预期配置 | 测试域名不存在或解析失败时,应用应按设计返回错误,不能无限重试或切换到错误地址 | 使用的 API、操作系统解析器、返回记录和业务日志 |
| 权限模型 | 使用应用原有的权限参数,验证明确允许的操作仍可执行 | 明确禁止的文件、子进程或网络访问应失败关闭 | 完整启动参数、二进制路径、允许与拒绝用例结果 |
Brotli 的正常压缩、解压往返只能证明升级没有明显破坏功能,不能证明恶意输入已经无法触发漏洞。安全结论仍要依赖官方安全版本和实际进程身份;不要为了验收公开复现可远程利用的请求。
DNS 回归也不能只执行一条与业务无关的命令。应用如果使用的是系统解析器路径,就按该路径测试;如果使用另一套解析接口,也要按实际调用路径测试。升级后出现“能启动但请求走错地址”,往往比直接 DNS 报错更隐蔽。
权限模型测试应沿用项目真实的启动参数和业务用例。不同 Node.js 大版本、发行版构建和应用封装方式可能使用不同入口,不能据此假定存在一个通用权限开关。若项目没有启用权限模型,应记录“不适用”,而不是为了覆盖安全新闻临时改变生产启动方式。
服务器证书链逐层检查
证书报错时应先检查网络边界,而不是先改 Node.js。在 Linux 或安装了 OpenSSL 的环境中,可以把 HOST 换成实际业务域名:
HOST="example.com"
openssl s_client -connect "$HOST:443" -servername "$HOST" -showcerts </dev/null
openssl s_client -connect "$HOST:443" -servername "$HOST" -verify_return_error </dev/null
第一条用于查看服务器实际发送的证书链,第二条用于查看 OpenSSL 当前的验证结果。不同 OpenSSL 构建、验证选项和系统配置可能得到不同结论,因此输出应原样保存,不要手工补写“Verify return code”。
如果证书链中出现受影响的 eMudhra 中间 CA,修复动作在服务器端:从合法 CA 取得新证书,或取得正确的完整证书链,然后更新真正终止 TLS 的 Web 服务、网关或反向代理。只改客户端信任库会继续拒绝这条链,不能算服务端修复。
如果 TLS 在反向代理终止,Node.js 应用可能只收到 HTTP 请求,运行时升级与浏览器握手错误没有直接关系。反向代理既要检查面向浏览器的下游证书链,也要检查它连接上游服务时使用的客户端信任配置。
OpenSSL 的验证结果也不能代替浏览器或 Node.js 的独立验证。浏览器可能使用自己的根证书集合,Node.js 构建、容器基础镜像和系统工具也可能使用不同的 CA 文件。一次外部探测成功,只能证明参与该探测的这一层成立。
客户端报错按来源定位
证书信任问题需要按层定位,不能看到 self signed certificate、unable to get local issuer certificate 或证书过期类错误,就统一执行关闭校验。
| 排查层 | 应检查的内容 | 不能代替的验证 |
|---|---|---|
| 浏览器 | 浏览器实际使用的信任集合、服务端证书链和失败页面 | 浏览器成功不代表 Node.js 成功 |
| 反向代理 | 下游服务端链、上游客户端信任源、代理自身运行镜像 | 代理更新不代表后端 Node.js 更新 |
| 操作系统 | 系统 CA 包及使用该信任源的本地程序 | 宿主机更新不代表容器内自动同步 |
| 容器镜像 | 镜像内的 Node.js、CA 文件和基础镜像摘要 | 临时容器成功不代表所有运行实例一致 |
| Node.js 应用 | 自定义 ca、NODE_EXTRA_CA_CERTS、验证开关及启动时加载的信任文件 |
应用可连接不代表其他客户端已恢复 |
可以先检查两个常见环境变量是否存在:
node -p 'process.env.NODE_EXTRA_CA_CERTS || "(unset)"'
node -p 'process.env.NODE_TLS_REJECT_UNAUTHORIZED || "(unset)"'
若使用 NODE_EXTRA_CA_CERTS,必须确认文件内容是业务明确拥有的私有信任链,并在进程启动时加载。临时加入信任文件后需要重启对应进程,验收完成后也不应把无关 CA 永久留在生产环境中。
还可以用一个很小的 Node.js TLS 客户端分别连接合法证书和受控的不受信任证书。保存为临时文件 tls-check.js:
const tls = require('node:tls');
const host = process.env.TLS_HOST;
if (!host) {
console.error('TLS_HOST is required');
process.exit(2);
}
const socket = tls.connect({
host,
port: 443,
servername: host,
rejectUnauthorized: true,
});
socket.setTimeout(10000, () => {
socket.destroy(new Error('TLS timeout'));
});
socket.once('secureConnect', () => {
console.log(JSON.stringify({
host,
authorized: socket.authorized,
authorizationError: socket.authorizationError,
}));
socket.end();
});
socket.once('error', (error) => {
console.error(JSON.stringify({
host,
code: error.code,
message: error.message,
}));
process.exitCode = 1;
});
Linux/macOS 中,把占位域名替换成实际测试地址后执行:
TLS_HOST="valid-host.example.com" node tls-check.js
TLS_HOST="untrusted-test-host.example.com" node tls-check.js
Windows PowerShell 使用:
$env:TLS_HOST = "valid-host.example.com"
node .\tls-check.js
$env:TLS_HOST = "untrusted-test-host.example.com"
node .\tls-check.js
第一个地址应使用合法证书;第二个地址必须是 DNS 可解析、但证书不受当前 Node.js 环境信任的受控测试端点。不要拿生产域名做错误证书实验。正例应成功完成验证,反例应返回非零退出状态,具体错误码和消息按实际结果保存,不应套用一份预设输出。
搜索“Node.js 如何加载根证书”时还会看到 2014 年 StackOverflow 的已采纳答案,它建议通过第三方模块 node-ssl-root-cas 加载自定义证书。 那只是特定旧场景下加载自有 CA 的思路,不能用来把本次已经进入撤销名单的中间 CA 再加入信任库;引入这类第三方模块本身也还需要独立的供应链审查。
不要通过关闭验证处理证书信任错误
不要把受影响的中间 CA 加回全局信任列表,也不要把 rejectUnauthorized 设为 false、把 NODE_TLS_REJECT_UNAUTHORIZED 设为 0,更不能把“关闭证书验证”写成生产兼容方案。这样虽然可能让报错消失,却会同时移除 TLS 对服务器身份的关键证明。
合法内网 CA 与这次事件中的危险证书不能混为一谈。内网 CA 应先核对签发者、信任锚、证书用途和业务归属,再以最小范围配置;不能因为错误信息里出现 unable to get local issuer certificate,就把所有私有 CA 全局加入系统。
升级后的兼容性问题应通过替换证书链、补齐正确的中间证书或更新对应信任源来处理,也可以等待应用与依赖发布兼容版本。无法立即升级旧运行时时,应依据官方公告采取已经确认的临时缓解措施,不能自行猜测补丁版本,也不能把回滚到已知易受攻击版本当成长期方案。
保存可重复执行的验收结果
最终验收至少要能在变更后重新执行同一组命令:
date -u
node -v
node -p "process.execPath"
node -p "process.versions"
Docker 候选镜像和运行中的实例仍应分别执行前面的 docker run 与容器内检查。每个实例需要留下以下信息:
| 验收项 | 必须记录的内容 |
|---|---|
| 运行时身份 | node -v、process.execPath、OpenSSL 与模块 ABI 信息 |
| 部署身份 | 主机、CI Job、镜像摘要或二进制安装来源、实际重启时间 |
| 依赖状态 | 锁文件提交、关键依赖版本、旧解压包是否已替换或移除 |
| 功能回归 | Brotli、TLS、DNS、权限模型的正常与反例结果 |
| 证书链 | 域名、签发者、服务端链、客户端信任来源及正反例结果 |
| 变更依据 | 官方安全公告 URL、对应安全版本、操作人和复核人 |
如果集群有多个实例,不能用其中一台的结果代表全部;如果灰度发布,也不能只验证新 Pod 而忽略旧实例。证书正例和反例应使用相同的目标环境、相同客户端版本和明确的操作条件,这样出现差异时才有可能定位到具体信任层。
验收不能只看版本号,实际进程和证书链都验证通过后,升级才算结束。












暂无评论内容