欢迎光临
我们一直在努力

从依赖分叉到远程指令源:JavaScript 供应链审计不能只看安装阶段

导语

“这个 npm 包没有安装脚本,沙箱安装时也没有异常网络请求。”

这句话只能证明一件事:在观察到的安装阶段,它没有表现出异常。它不能证明模块加载后、业务连接建立后或应用运行数小时后仍然安全。

OpenSSF 于 2026 年 9 月 3 日记录的恶意包 @modss/baileys,提供了一个很典型的反例。该包是 WhatsApp Web 库 Baileys 的重命名分叉。它不依赖 preinstall 或 postinstall,而是在 WhatsApp 会话成功连接后获取发布者控制的远程频道列表,再用安装者自己的认证会话执行关注操作。

攻击逻辑因此同时避开了三种常见但不完整的判断:

  • 没有生命周期脚本,所以 –ignore-scripts 不会触发它;

  • 安装时尚未建立业务会话,所以短时沙箱可能看不到最终动作;

  • npm 制品本身不保存固定目标,所以同一哈希在不同时间可能表现不同。

真正的问题不是团队缺少扫描器,而是扫描模型把“依赖风险”错误地压缩成了“安装风险”。

本文将以 MAL-2026-15918 为案例,建立一套面向 JavaScript 依赖的四阶段审计框架:安装期、加载期、业务初始化期和持续运行期。


一、先划清事实边界

已确认事实

OpenSSF、OSV 与 GitHub Advisory Database 记录的信息包括:

  • 包名为 @modss/baileys;

  • 已确认版本为 1.0.0、1.0.1 和 1.0.2;

  • 三个版本均包含相同的相关处理逻辑;

  • 代码位于 lib/Socket/newsletter.js;

  • 当 connection.update 事件中的连接状态变为 open 时,代码会获取远程 JSON 列表;

  • 它会通过当前已认证的 WhatsApp 会话,对列表中的频道执行 FOLLOW;

  • 目标列表由包发布者在 npm 发版之外控制,可以在不重新发布包的情况下变化;

  • 没有 npm 生命周期脚本参与这条触发链;

  • 上游正版 @whiskeysockets/baileys 不受这个分叉包事件影响;

  • OSV 和 GitHub Advisory Database 没有列出修复版本。

没有被证实的能力

现有公开分析没有观察到该包窃取凭据或会话密钥,也没有证据显示它执行任意系统命令。

因此,本文不会把它描述为“远程代码执行木马”或“WhatsApp 密钥窃取工具”。已经确认的是未经用户同意、借用现有登录态执行账户操作。

关于来源

OpenSSF 恶意包记录发布于 2026 年 9 月 3 日,OSV 于 9 月 4 日更新,GitHub Advisory Database 于 9 月 4 日收录并复核。发现方 pkgwarden 表示,其分析采用下载 tarball、解包和逐文件静态阅读的方式,没有安装、导入或执行可疑包,也没有请求包中发现的远程地址。

这点很重要:研究结论来自对可达代码路径的静态验证,而不是运行真实恶意逻辑。


二、传统 npm 安全检查为什么会漏掉它

第一种错觉:没有 postinstall 就没有执行风险

npm 生命周期脚本确实是供应链攻击的常见入口。团队检查 package.json 中的 preinstall、install 和 postinstall,或者统一使用:

npm ci –ignore-scripts

都能消除一部分风险。

但 JavaScript 依赖还可以在以下时刻执行代码:

  • require() 或 import 时的模块顶层;

  • 工厂函数和 SDK 初始化时;

  • 网络连接成功后的事件回调中;

  • 定时器、重连逻辑或后台轮询中;

  • 首次调用某个业务方法时;

  • 传递依赖被解析或动态加载时。

–ignore-scripts 只控制包管理器执行生命周期脚本,不会禁止应用在正常运行中加载依赖代码。

第二种错觉:沙箱跑一分钟没外连就安全

如果恶意逻辑等待:

  • 有效账户完成登录;

  • WebSocket 或长连接进入 open;

  • 延迟 30 到 120 秒;

  • 某个业务事件第一次出现;

  • 生产环境变量、真实令牌或特定域名存在;

短时、无凭据、无业务流量的动态分析就可能得出假阴性。

这不是动态分析没有价值,而是触发条件没有被满足。报告应该写“在这些条件下未观察到”,不能直接写“该包没有恶意行为”。

第三种错觉:锁文件与哈希锁住了行为

锁文件和完整性哈希能够回答:

  • 安装了哪个包、哪个版本;

  • 下载的 tarball 是否被中途替换;

  • 构建是否使用了预期制品。

但如果固定代码会从外部读取目标、策略、模板或命令,同一制品仍然可以在不同时间产生不同结果。

固定 tarball + 可变远程配置 = 可变运行时行为

因此,软件物料清单需要扩展为“代码制品 + 外部控制源 + 权限边界”,而不能停在包名和版本。


三、从分叉包到远程指令源:完整信任链如何形成

第一步:名称与功能继承信任

分叉包往往保留上游项目的 API、文档和大部分代码。开发者看到熟悉的接口、示例和生态名称,容易把对上游的信任迁移到新的 npm scope。

但以下对象彼此独立:

  • 上游 Git 仓库;

  • 分叉仓库;

  • npm 发布账号;

  • 实际上传的 tarball;

  • 运行时访问的远程资源。

名称相似不能建立它们之间的可信绑定。

第二步:正常事件成为恶意触发器

connection === "open" 本来是一个合理的业务事件。恶意分叉利用它,是因为此时两个条件已经满足:

  • 宿主应用认为 SDK 初始化成功;

  • 当前进程已经持有可用认证会话。

  • 恶意行为混在正常连接回调中,比安装阶段突然启动一个陌生进程更不显眼。

    第三步:远程列表把制品外数据变成动作选择器

    远程 JSON 表面上只是数据,但它决定了“对哪个目标执行关注”。如果发布者可以随时改变列表,就拥有了持续影响已部署实例的能力。

    远程内容无需包含 JavaScript。只要它能够控制以下任一元素,就具备指令属性:

    • 动作类型;

    • 动作目标;

    • 执行时间;

    • 权限范围;

    • 要加载的后续模块或地址。

    第四步:依赖借用宿主身份完成副作用

    恶意代码没有必要先导出会话密钥。它已经位于宿主进程内,可以调用相同 SDK,使用用户当前身份完成动作。

    这可以概括为:

    发布者控制的外部输入

    第三方依赖的事件处理器

    宿主应用持有的认证会话

    用户未授权的外部副作用

    依赖真正获得的不是“一个函数调用权限”,而是进程能够触达的全部能力集合。


    四、四阶段审计模型

    阶段一:发布与安装期

    检查目标:谁发布了什么制品,安装动作会发生什么。

    重点包括:

    • npm scope 和发布者账号的创建历史;

    • 包名是否模仿或重命名知名项目;

    • 仓库、Git Tag 与 npm tarball 是否一致;

    • 生命周期脚本、二进制下载和原生扩展;

    • 锁文件、完整性哈希和 registry 来源;

    • 新增依赖、别名依赖及 Git/URL 依赖。

    阶段结论只能是“安装期风险评估”,不能替代后续阶段。

    阶段二:模块加载期

    检查目标:import 或 require 时是否产生副作用。

    需要搜索:

    • 模块顶层网络请求和文件写入;

    • 自动注册的事件监听器;

    • setTimeout、setInterval 和后台任务;

    • monkey patch、prototype 修改和全局钩子;

    • 依赖别名是否在加载时修改另一个合法包;

    • 动态 require、import()、eval 和 Function。

    测试时要把“只解包”“导入模块”“实例化客户端”分成不同步骤,分别记录行为。

    阶段三:业务初始化期

    检查目标:获得真实配置、连接和身份后,依赖做了什么。

    典型触发条件包括:

    • 登录完成;

    • 数据库或消息队列连接成功;

    • WebSocket 进入 open;

    • CI 获取仓库令牌;

    • 云 SDK 装载默认凭据;

    • AI Agent 完成工具注册。

    @modss/baileys 的已确认行为位于这一阶段。没有模拟连接成功事件,就无法覆盖关键路径。

    阶段四:持续运行期

    检查目标:同一制品的行为是否会被时间和外部状态改变。

    重点观察:

    • 周期性外连和长连接;

    • 远程 JSON、YAML、策略和模板;

    • DNS、重定向和短链变化;

    • 运行时下载的插件、模型或脚本;

    • 本地标记文件与持久化状态;

    • 重连、重试、更新和故障回退路径。

    这一阶段要求持续遥测,而不是发布前扫描一次就结束。


    五、无害实验:同一哈希为何不等于同一行为

    下面的 Node.js 模型不连接网络,不调用 WhatsApp,也不包含真实频道 ID。两个“远程响应”由本地对象代替,用来证明固定代码会因外部数据变化而产生不同动作。

    const crypto = require('node:crypto');

    function packageHash(source) {
    return crypto.createHash('sha256').update(source).digest('hex');
    }

    function unsafeHandler(remoteList, authenticatedSession) {
    if (!authenticatedSession) return [];
    return remoteList.map(target => ({ action: 'FOLLOW', target }));
    }

    function safeHandler(remoteList, context) {
    const allowed = new Set(['official-status']);

    if (!context.authenticated) return [];
    if (!context.userApproved) return [];
    if (!context.configSignatureValid) return [];

    return remoteList
    .filter(target => allowed.has(target))
    .map(target => ({ action: 'FOLLOW', target }));
    }

    const fixedSource = unsafeHandler.toString();
    console.log('same package hash:', packageHash(fixedSource));

    console.log('day 1:', unsafeHandler(['official-status'], true));
    console.log('day 2:', unsafeHandler(['publisher-selected'], true));

    console.log('safe day 2:', safeHandler(['publisher-selected'], {
    authenticated: true,
    userApproved: false,
    configSignatureValid: true
    }));

    安全版本同时要求:

    • 配置来源可验证;

    • 用户明确同意;

    • 目标在服务端或本地允许列表中;

    • 所有副作用可审计。

    即便签名有效,也不能省略用户授权和目标约束。签名只能证明“内容来自谁”,不能证明“内容允许做什么”。


    六、如何避免静态分析的两种误判

    误判一:看见可疑字符串,就认定一定被执行

    研究者在同类分叉审查中发现过反例:可疑代码只存在于备份文件,Node.js 不会加载;或者写入逻辑存在,但没有任何调用路径。

    可靠结论需要证明:

    入口 → 注册 → 触发条件 → 可达函数 → 有副作用的 sink

    仅凭域名、函数名或代码片段,最多能形成线索,不能直接证明真实行为。

    误判二:关键词没命中,就认定没有行为

    同一种关注动作可能通过高级封装函数调用,也可能直接调用底层查询接口;可疑代码还可能位于与真实用途不符的文件中。

    因此审计不应只搜索 newsletterFollow、postinstall 或某个已知域名。更稳妥的方法是从能力 sink 反向追踪:

    • 哪些函数会修改外部账户状态?

    • 谁调用了它们?

    • 参数来自硬编码、用户输入还是远程响应?

    • 调用路径是否在默认配置下可达?


    七、开发与安全团队可执行的行动清单

    P0:处置本次事件

  • 在 package.json、锁文件、SBOM、构建缓存和容器镜像中搜索精确包名 @modss/baileys。

  • 发现 1.0.0、1.0.1 或 1.0.2 时直接移除;当前没有官方列出的修复版本。

  • 不要随意替换为另一个名称相似的 Baileys 分叉。业务确有需要时,应重新选择并验证可信来源。

  • 重新构建制品,检查旧包是否残留在镜像层、离线缓存、Serverless Layer 或共享 node_modules 中。

  • 由账户所有者检查异常频道关注及相关操作记录。

  • 现有分析没有确认会话密钥泄露,因此是否轮换会话应结合其他异常证据和环境风险决定;不要把“安装过”机械等同于“凭据已经外泄”。

    P1:扩展检测范围

    • 在代理、DNS 和 eBPF/容器网络遥测中识别依赖的运行时外连;

    • 对 GitHub Raw、对象存储、Paste 服务和短链接收紧出站策略;

    • 监控 SDK 建立认证会话后的非业务预期动作;

    • 标记依赖创建的定时器、后台任务和长连接;

    • 对“远程响应 → 敏感 API”建立污点分析规则;

    • 保留包 tarball 和哈希,支持事后逐版本比较。

    P2:把四阶段审计接入 DevSecOps

    CI 准入
    • 拒绝已进入 OSV/OpenSSF 恶意包数据集的精确版本;

    • 对新 scope、重命名分叉和突然更换发布者的包提高审核等级;

    • 比较 tarball 与声明源码仓库,而不是只检查版本号;

    • 展开 npm alias、workspace、Git URL 和传递依赖。

    隔离测试
    • 阶段 1:只下载和解包,记录文件、脚本和哈希;

    • 阶段 2:导入模块,记录顶层副作用;

    • 阶段 3:模拟登录、连接成功和首次业务调用;

    • 阶段 4:延长观察时间,模拟重连并改变远程配置响应。

    测试报告必须写清楚触发条件、凭据状态、网络策略和观察时长。没有命中行为时,应写“未观察到”,而不是无条件写“安全”。

    运行时约束
    • 将第三方插件和机器人库放入最小权限进程;

    • 对出站网络采用默认拒绝;

    • 对发送、关注、发布、删除等动作设置独立授权;

    • 把远程配置纳入签名、版本、模式和允许列表校验;

    • 对配置变更保留审计记录,并支持快速撤销。


    八、建议建立的供应链测试矩阵

    阶段测试条件主要观察项典型漏报原因
    安装期 有/无生命周期脚本 进程、网络、文件写入 只覆盖包管理器行为
    加载期 require / import 顶层副作用、定时器、补丁 测试只解包未加载
    初始化期 有效配置与模拟认证 事件处理器、SDK 副作用 沙箱没有真实触发条件
    运行期 长时运行、重连、配置变化 周期外连、远程控制源 观察窗口过短
    故障期 网络失败、签名错误、超时 是否安全失败 回退逻辑扩大权限

    矩阵的价值不是让每个依赖都跑一套昂贵测试,而是根据能力分级:普通工具库做轻量审查;持有账户、云、CI/CD、支付或 AI 工具权限的 SDK,必须覆盖全部阶段。


    总结

    @modss/baileys 的案例表明,恶意依赖完全可以在安装阶段保持安静,等待宿主获得有效会话后再行动;它还可以把目标移到 npm 制品之外,让发布后的外部数据持续改变行为。

    这要求供应链防线回答四个不同问题:

  • 安装时执行了什么?

  • 模块加载时注册了什么?

  • 获得业务身份后调用了什么?

  • 运行期间谁还能改变它的行为?

  • 锁文件、哈希、–ignore-scripts、SCA 和短时沙箱各自只能回答其中一部分。真正有效的依赖治理,需要把静态来源、可达代码、业务触发条件、外部控制源和运行时权限放在同一张图上。

    代码是否固定,只是供应链可信的一半。另一半是:固定代码运行时还会听谁的。

    赞(0)
    未经允许不得转载:171主机测评 » 从依赖分叉到远程指令源:JavaScript 供应链审计不能只看安装阶段
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址