本内容是对color npm package compromised 的整理与翻译


事件经过

2025 年 9 月 8 日,北京时间 21:00 前后,一场针对 npm 生态的供应链攻击悄然展开。

攻击者入侵了开发者 Josh Junon(npm 用户名 qix)的 npm 账号,并开始向他名下所有热门包发布含有后门的恶意版本。

最早察觉到异常的,是另一位开发者 Charlie Eriksen。他在 BlueSky 上发帖提醒 Josh:

“嘿,你的 npm 账号好像被入侵了。一小时前,有人开始向你的所有热门包推送带有后门的版本。”

Josh 很快确认了这一消息,并道出了被入侵的经过——他被一封伪造的双因素认证(2FA)重置邮件给钓鱼了:

“是的,我被入侵了。是一封 2FA 重置邮件,看起来非常正规。只有 NPM 受到影响。我已经给 @npmjs 发邮件,希望能重新拿回账号控制权。抱歉大家了,我应该更仔细一点的。这周压力太大,不太像我平时的状态。我会尽力把这件事处理好的。”


钓鱼邮件是什么样的

这封钓鱼邮件来自域名 npmsj.help,该域名在攻击发生前仅三天才注册。

邮件内容以"账号安全"为由,要求用户更新双因素认证凭据,并声称若不在规定日期前完成操作,账号将被临时锁定:

“Hi, qix!作为我们持续承诺账号安全的一部分,我们要求所有用户更新其双因素认证(2FA)凭据。我们的记录显示,您的 2FA 上次更新距今已超过 12 个月。为维护您账号的安全性与完整性,我们诚恳请求您尽快完成此次更新。请注意,自 2025 年 9 月 10 日起,2FA 凭据过期的账号将被临时锁定,以防止未经授权的访问。立即更新 2FA”

这封邮件通过 Mailtrap 发送,原始邮件内容已公开。Josh 本人也把完整的原始邮件内容贴到了 GitHub Gist 上供安全研究人员参考。

注意这封邮件的几个特征:

  • 域名 npmsj.helpnpmjs 的高仿,字母顺序被调换(npmjs → npmsj)
  • 措辞专业,语气紧迫但礼貌
  • 给出了具体的锁定日期(9 月 10 日),制造时间压迫感
  • Josh 自称那周压力很大,状态不好,警惕心下降

这是一次典型的、精心设计的针对性网络钓鱼攻击。


受影响的包有哪些,规模有多大

安全研究员 Kevin Beaumont 在 Mastodon 上整理了一份受影响包的名单:

  • supports-hyperlinks
  • chalk-template
  • simple-swizzle
  • slice-ansi
  • error-ex
  • is-arrayish
  • wrap-ansi
  • backslash
  • color-string
  • color-convert
  • color
  • color-name

这些包,在被投毒之前,累计下载量已接近十亿次

其中仅 color 一个包,每周下载量就高达约 3200 万次

color 是 Node.js 生态中用于颜色处理的基础工具包,chalk、chalk-template、supports-hyperlinks 等都是开发者日常使用的知名工具库。这些包的依赖树遍布整个 JavaScript 生态——某种程度上,几乎任何一个有一定规模的前端或 Node.js 项目,都可能间接依赖其中的某几个。


恶意载荷的分析:这是一个加密货币窃取器

攻击者植入的恶意代码(payload)完整版本已被公开在 Pastebin,Amos 随后对其进行了反混淆处理,并在 GitHub Gist 上发布了可读版本,还专门写了一个松散移植的 TypeScript 版本(命名为 0x112)来搞清楚代码的意图。

关键结论:这个 payload 不是针对服务器端 Node.js 环境的,而是针对浏览器端的。

这意味着,要让这次攻击真正生效,需要满足一条相当特定的攻击路径:

  1. 某个加密货币相关的 Web 应用的维护者,合并了一个升级了上述依赖的 PR
  2. 这些恶意依赖被打包进了前端代码(而不是只在服务端使用)
  3. 构建产物被部署上线
  4. 真实用户在使用该网站时,恰好发起了加密货币交易

也就是说,如果有人只是接受了一个依赖升级的 PR、在 CI 上跑了一下测试,那目前什么实际的损失都还没发生。

但各方仍在继续分析确认所有受影响的包,情况仍有变数。


恶意代码具体做了什么

反混淆之后的代码逻辑分为两大部分:

第一部分:劫持 HTTP 响应中的加密地址

恶意代码对浏览器原生的 fetchXMLHTTPRequest 进行了 monkey-patch(即在运行时替换这两个全局函数的实现)。

一旦任何 HTTP 请求的响应体中出现看起来像加密货币地址的字符串,该地址就会被静默替换成攻击者控制的地址。

支持替换的加密货币类型包括:Bitcoin、Solana、Litecoin v2 等主流加密货币。

注意:代码只修改响应体,不修改请求体。这背后的推测是:某些加密货币交易应用会先通过 API 查询"应该转账到哪个地址",然后再通过其他机制完成转账。攻击者在 API 响应的中间截断这个地址,就能把钱导向自己的钱包。

第二部分:劫持 MetaMask(以太坊)

每隔 500 毫秒,代码就会调用 window.ethereum.request 来检查是否有以太坊账号通过 MetaMask 授权给当前页面使用。

一旦检测到已授权的账号,代码就会对 window.ethereum 进行 monkey-patch,篡改所有即将发出的交易参数,将资金导向攻击者的地址。

具体针对的以太坊操作包括:

  • approve(address,uint256)(函数选择器 0x095ea7b3):替换目标地址,并将授权额度拉满至最大值。代码里还专门识别了主流 DEX 的名字,包括 Uniswap、PancakeSwap、1inch、SushiSwap。
  • permit(address,address,uint256,uint256,uint8,bytes32,bytes32)(函数选择器 0xd505accf):替换目标地址,并将金额拉满至最大值。
  • transfer(address,uint256)(函数选择器 0xa9059cbb):替换目标地址,金额保持原样。
  • transferFrom(address,address,uint256)(函数选择器 0x23b872dd):替换目标地址,金额保持原样。

此外代码中还包含针对 Solana 的逻辑,会将相关字段替换为 19111111111111111111111111111111,但目前尚不确定这条路径是否能真正成功执行。

一言以蔽之:这是一个针对加密货币 Web 前端的全方位资产窃取器,依赖 npm 包的供应链作为初始感染途径,在浏览器端悄无声息地把用户的转账地址替换掉。


事件时间线与 npm 官方的应对

北京时间约 21:00:攻击开始。

约 23:00(事发约 2 小时后):Josh 仍然被锁在自己的 npm 账号之外,npm 官方团队迟迟没有回应邮件。

Josh 在 Hacker News 上发帖说:

“已经将近两个小时了,npm 连一封回复邮件都没有。我坐在这里,不知道该怎么做来修复这一切。那些有 Sindre 作为共同发布者的包已经被他覆盖发布了一个干净版本,但即便是他,好像也没办法把恶意版本从 npm 上撤掉(AFAIU)。如果有人有什么建议,我洗耳恭听。”

其中提到的 Sindre,是著名 JavaScript 开源开发者 Sindre Sorhus。他对 chalk 包有共同发布权限,第一时间发布了一个干净版本覆盖了恶意版本。但其他包,比如 simple-swizzle,在报道截止时仍处于被入侵状态,npm 上仍可以看到以 const _0x112fa8 开头的混淆代码。

约 23:19:npm 官方联系了 Josh,表示正在处理,将移除受影响的包版本。


这次事件暴露了什么问题

npm 生态的信任模型是脆弱的

整个事件的触发点只是一个人的账号被钓鱼了。Josh 发布的包,每周被下载几千万次,信任的基础仅仅建立在"这个账号属于 Josh"这一个假设上。

一旦这个假设被打破——无论是因为钓鱼、密码泄露、Session 劫持还是其他方式——整条下游依赖链都面临暴露。

供应链攻击的目标变了

传统的 npm 供应链攻击(比如几年前的 event-stream 事件)往往针对服务器端的 Node.js 环境,目标是窃取环境变量、私钥等信息。

而这次攻击的载荷明确指向浏览器端,瞄准的是加密货币用户。这说明攻击者越来越精准:他们知道大量加密货币应用是 JavaScript 前端写的,也知道这些应用的依赖树里大概率有 color 这类基础包。

2FA 不是万能的,但仍然重要

Josh 是有 2FA 的,但攻击者通过伪造的 2FA 重置流程绕过了这个保护。这说明:

  • 账号安全的薄弱环节往往不是 2FA 本身,而是重置流程
  • 在压力大、注意力不集中的状态下操作高权限账号是危险的
  • 收到任何要求重置 2FA 的邮件,都应该通过官网直接登录验证,而不是点击邮件里的链接

npm 的危机响应速度令人担忧

事发两小时内,npm 官方没有任何回音。对于一个每周数十亿次下载的关键基础设施,这个响应速度是不够的。

对比来看,个人开发者 Sindre Sorhus 几乎在第一时间就发布了 chalk 的干净覆盖版本;而 npm 平台本身,作为一个有商业背景(GitHub → Microsoft)的基础设施,在危机处理上的响应效率,显然不尽如人意。


如果你的项目依赖了这些包,该怎么办

第一步:确认影响范围

检查你的项目是否直接或间接依赖了以下包:colorcolor-stringcolor-convertcolor-namesimple-swizzlechalk-templatesupports-hyperlinksslice-ansiwrap-ansierror-exis-arrayishbackslash

# 用 npm 检查依赖树
npm ls color

# 或者用 yarn
yarn why color

第二步:判断风险等级

  • 如果这些包只用于服务端(Node.js 环境),且没有被打包进浏览器端代码:当前没有直接风险,因为恶意载荷是面向浏览器的。
  • 如果这些包被打包进了前端代码,且你的应用涉及加密货币相关功能:需要立即排查,查看是否在被投毒的时间窗口内构建并部署了含有恶意代码的版本。

第三步:锁定或升级依赖

确认受影响版本号,升级到已修复的干净版本,或临时锁定到未受影响的旧版本。

第四步:审计已部署的前端资产

如果你在被投毒时间窗口内(约 UTC 13:00 至事后 npm 移除恶意版本之间)重新构建并部署了前端代码,应当:

  • 立即重新构建和部署
  • 审计这段时间内是否有用户发起了加密货币相关的操作

结语

这次事件是供应链安全问题的又一次教科书级别的演示。攻击者不需要攻破 npm 的服务器,也不需要给每个项目单独注入恶意代码——只需要控制一个高影响力的 npm 账号,就可以把毒素注入到整个依赖树中。

对于普通开发者来说,这件事的教训很简单:

  • 收到"请重置 2FA"的邮件,先停下来,去官网自己登录,不要点邮件里的链接。
  • 自动化的依赖更新 PR(比如 Dependabot、Renovate)需要认真审查,而不是一看测试绿了就合并。
  • 尽可能使用 package-lock.jsonyarn.lock 锁定版本,并定期用工具(如 npm audit)检查已知漏洞。

npm 生态的规模决定了它是一个永久性的攻击面。下一次投毒,可能随时到来。


原文链接:https://fasterthanli.me/articles/color-npm-package-compromised
反混淆载荷:https://gist.github.com/fasterthanlime/eba5b06c9cf2b39a525c51ae41ffcc00
TypeScript 分析版本:https://github.com/fasterthanlime/0x112
钓鱼邮件原文:https://gist.github.com/Qix-/c1f0d4f0d359dffaeec48dbfa1d40ee9/

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐