导语:这起编号为 CVE-2026-107243 的硬编码会话密钥漏洞,把「一个 secret 打天下」这种最典型的工程偷懒,直接放大成了全网设备可被同一张伪造 Cookie 通吃的横向越权灾难。VOS(原 Versa FlexVNF)设备上用于 URL 零接触部署(ZTP)的 Node.js Web 服务,在 server.js 中为 express-session 配置了跨所有设备完全一致的硬编码 secret,攻击者只要拿到任意一个有效 session ID,就能离线算出正确的 HMAC-SHA256 签名,伪造出被任意 VOS 设备认可的会话 Cookie。本文以架构师视角完整拆解这条攻击链,并给出从密钥治理到服务生命周期收敛的三层防御落地路径。
一、事件全景:硬编码会话密钥漏洞的暴露面与影响半径
先把事实钉死。该漏洞位于 VOS 设备内置的 Node.js Web 服务,其职责是为设备提供基于 URL 的零接触部署(ZTP)能力——设备上电后从该服务拉取配置完成初始化。按厂商安全加固指南,ZTP 完成后该服务本应被关闭,但现实中大量部署并未执行这一步,于是这个「临时服务」长期在线,成为稳定的攻击面。
漏洞的技术根因极其朴素:server.js 中 express-session 的 secret 被写死为常量,且该常量在所有 VOS 设备上完全相同。express-session 用这个 secret 对 connect.sid 做 HMAC-SHA256 签名,服务端校验时只验签、不区分设备。这意味着签名密钥一旦泄露(或本身是公开常量),签名就退化为「可公开计算」的校验和,安全属性完全丧失。
| 评估维度 | 本漏洞实际表现 | 安全基线要求 | 风险判定 |
|---|---|---|---|
| 密钥唯一性 | 全网设备共用同一硬编码 secret | 每实例独立随机生成 | 严重违规 |
| 密钥可轮换性 | 写死在源码,无法轮换 | 支持热轮换与多密钥并存 | 严重违规 |
| 服务生命周期 | ZTP 完成后仍长期在线 | 按需启用、用完即关 | 高风险 |
| 攻击影响范围 | 单点泄露即全网设备通用 | 故障域隔离到单实例 | 严重违规 |
| CVSS 评分 | 7.6(High) | — | High |
很多团队把 ZTP 服务当作「部署期临时组件」,默认认为它不重要、不设防。恰恰是这种认知偏差,让一个本应只存活几分钟的服务变成了长期驻留的高危入口。任何「临时」服务都必须有明确的生命周期终止策略,否则它迟早会变成攻击者的跳板。
二、攻击链拆解:从 session ID 到全网通吃的伪造 Cookie
要理解危害,必须把 express-session 的签名机制摊开看。默认配置下,会话 Cookie 形如 s:<sessionID>.<signature>,其中 signature 是 HMAC-SHA256(secret, sessionID) 的 base64 编码。服务端收到 Cookie 后,用同一 secret 重算签名并做定长比较,一致即认为会话合法。
关键点在于:签名只证明「这个 session ID 是被持有 secret 的一方签发的」,并不证明「这个 session ID 属于当前请求者」。因此攻击者只需两步:其一,从任意渠道获得一个有效 session ID(日志泄露、共享链接、抓包、甚至自己注册获得的会话);其二,用公开的硬编码 secret 离线计算签名,拼出完整 Cookie。由于 secret 全网一致,这张伪造 Cookie 在部署内任意 VOS 设备上都能通过验签。
企业级架构规范
flowchart TD
subgraph 攻击面 ["攻击者视角与情报获取层 (Attacker Tier)"]
A["获取任意有效 session ID"] --> B["读取公开硬编码 secret"]
B --> C["离线计算 HMAC-SHA256 签名"]
end
subgraph 接入层 ["设备接入与验签层 (Gateway Tier)"]
C --> D["构造 s:sessionID.signature Cookie"]
D --> E["VOS Node.js ZTP 服务验签"]
end
subgraph 规则层 ["会话校验与授权层 (Session Tier)"]
E --> F{"secret 是否全局一致"}
F -->|"是"| G["验签通过 伪造会话生效"]
F -->|"否"| H["验签失败 请求拒绝"]
end
subgraph 防御层 ["纵深防御与加固层 (Defense Tier)"]
G --> I["横向越权 全网设备失陷"]
H --> J["密钥轮换 服务下线 网关限流"]
end
2.1 为什么 CVSS 是 7.6 而不是更高
CVSS v3 向量显示攻击向量为网络、攻击复杂度高、所需权限高、需要用户交互,但机密性、完整性、可用性均为高,且 Scope 为 Changed。这组指标说明:漏洞本身不提供「一键 getshell」,攻击者仍需先获得一个有效 session ID 并完成签名计算,门槛不低;但一旦成功,影响跨越安全域,后果是全量级的。这也解释了为什么它被评为 High 而非 Critical——利用有前置条件,但破坏力真实且广泛。
不要因为「需要先拿到 session ID」就低估此漏洞。在设备日志、监控采集、共享运维链接、甚至浏览器历史中,session ID 的泄露路径远比想象中多。硬编码 secret 把「拿到 ID」直接等价于「拿到会话」,中间没有任何第二道防线。
三、根因剖析:express-session 的 secret 到底该怎么管
express-session 的设计本身没有问题,问题出在配置方式。它的 secret 支持字符串或数组,数组形式天然支持多密钥并存与平滑轮换——新密钥放首位用于签名,旧密钥保留用于验签,待旧会话自然过期后移除。这个机制恰恰是修复硬编码会话密钥漏洞最直接的抓手,但前提是密钥必须来自外部注入而非源码常量。
const session = require('express-session');
const crypto = require('crypto');
const fs = require('fs');
// 从受保护的外部文件/密钥管理服务加载,禁止硬编码
function loadSecrets() {
const raw = fs.readFileSync('/etc/vos/ztp/session-secrets.json', 'utf8');
const parsed = JSON.parse(raw);
// 数组首位为当前签名密钥,其余为历史验签密钥
return parsed.secrets.map(s => s.value);
}
app.use(session({
name: 'connect.sid',
secret: loadSecrets(), // 支持多密钥轮换
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // 强制 HTTPS
sameSite: 'strict',
maxAge: 30 * 60 * 1000 // 短生命周期,降低泄露窗口
}
}));
配套的密钥文件权限必须收紧到仅服务账户可读,且每次设备初始化时由本地 CSPRNG 生成独立密钥,绝不允许跨设备复用。若设备具备 TPM 或安全芯片,密钥应封装在硬件信任根内,进一步抬高提取门槛。
四、三层防御落地:从密钥治理到服务生命周期收敛
4.1 第一层:密钥治理——消灭全局常量
核心原则是「一机一密、可轮换、不可硬编码」。落地清单如下:
- 移除源码中所有 session secret 常量,改为启动时从外部密钥源加载。
- 设备首次初始化时用
crypto.randomBytes(32)生成独立密钥,写入受保护路径。 - 采用数组形式配置 secret,建立定期轮换流程(建议 90 天),轮换期间新旧密钥并存。
- 密钥文件权限 600,属主为服务运行账户,禁止进入备份与镜像。
4.2 第二层:服务生命周期——用完即关
厂商加固指南明确建议:ZTP 完成后关闭 Node.js Web 服务。这不是可选项,而是消除攻击面的根本手段。工程上应把「关闭 ZTP 服务」编排进部署流水线的最后一步,并加入配置巡检,发现未关闭即告警。
点击展开:ZTP 服务下线与巡检脚本
# 1. 部署完成后停止并禁用 ZTP Web 服务 systemctl stop vos-ztp-web.service systemctl disable vos-ztp-web.service # 2. 巡检:确认服务未监听 if ss -lntp | grep -q ':8443'; then echo "[ALERT] ZTP web service still listening on 8443" | logger -t ztp-audit fi # 3. 防火墙兜底:仅允许部署网段访问 # iptables -A INPUT -p tcp --dport 8443 -s 10.0.0.0/8 -j ACCEPT # iptables -A INPUT -p tcp --dport 8443 -j DROP
4.3 第三层:网关加固——让伪造 Cookie 无处可用
即便密钥治理到位,也应在接入层叠加纵深防御:强制 HTTPS 与 HSTS、对会话接口做速率限制、对异常 Cookie 模式(如短时间内大量不同 session ID 来自同一源)触发告警。这些措施不能替代密钥修复,但能在修复窗口期内显著压缩攻击者的操作空间。
修复优先级应为:先下线服务(最快消除暴露面)→ 再轮换密钥(阻断已泄露密钥的利用)→ 最后补网关与巡检(长期防御)。不要反过来先做复杂改造而让服务继续裸奔。
五、横向升华:硬编码会话密钥漏洞背后的通用工程教训
这起硬编码会话密钥漏洞不是孤例,它折射出三类高频工程反模式:其一,把「临时/内部」服务排除在安全基线之外;其二,把密钥当作配置而非机密,写进源码与镜像;其三,缺乏跨实例的故障域隔离意识,让单点泄露演变为全局失陷。任何一条踩中,都会让再完善的业务逻辑防护形同虚设。
对架构师的启示是明确的:会话签名密钥属于最高等级的机密资产,必须纳入密钥管理体系(KMS/HSM),具备唯一性、轮换性与可审计性;同时,所有服务的生命周期都应有明确的终止条件与自动化执行路径。把这两件事做成平台能力,而不是靠每个开发者自觉,才是根治之道。
本文核心技术背景源自 GitHub Advisory Database 与 NVD 官方披露:
GHSA-q454-c9v2-qxv3 (GitHub Advisory Database)
CVE-2026-107243 (NVD)
