npm 生态遭遇供应链攻击:color 包被投毒,每周 3200 万次下载全部受影响
这次事件是供应链安全问题的又一次教科书级别的演示。攻击者不需要攻破 npm 的服务器,也不需要给每个项目单独注入恶意代码——只需要控制一个高影响力的 npm 账号,就可以把毒素注入到整个依赖树中。收到"请重置 2FA"的邮件,先停下来,去官网自己登录,不要点邮件里的链接。自动化的依赖更新 PR(比如 Dependabot、Renovate)需要认真审查,而不是一看测试绿了就合并。尽可能使用或yar
本内容是对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.help是npmjs的高仿,字母顺序被调换(npmjs → npmsj) - 措辞专业,语气紧迫但礼貌
- 给出了具体的锁定日期(9 月 10 日),制造时间压迫感
- Josh 自称那周压力很大,状态不好,警惕心下降
这是一次典型的、精心设计的针对性网络钓鱼攻击。
受影响的包有哪些,规模有多大
安全研究员 Kevin Beaumont 在 Mastodon 上整理了一份受影响包的名单:
supports-hyperlinkschalk-templatesimple-swizzleslice-ansierror-exis-arrayishwrap-ansibackslashcolor-stringcolor-convertcolorcolor-name
这些包,在被投毒之前,累计下载量已接近十亿次。
其中仅 color 一个包,每周下载量就高达约 3200 万次。
color 是 Node.js 生态中用于颜色处理的基础工具包,chalk、chalk-template、supports-hyperlinks 等都是开发者日常使用的知名工具库。这些包的依赖树遍布整个 JavaScript 生态——某种程度上,几乎任何一个有一定规模的前端或 Node.js 项目,都可能间接依赖其中的某几个。
恶意载荷的分析:这是一个加密货币窃取器
攻击者植入的恶意代码(payload)完整版本已被公开在 Pastebin,Amos 随后对其进行了反混淆处理,并在 GitHub Gist 上发布了可读版本,还专门写了一个松散移植的 TypeScript 版本(命名为 0x112)来搞清楚代码的意图。
关键结论:这个 payload 不是针对服务器端 Node.js 环境的,而是针对浏览器端的。
这意味着,要让这次攻击真正生效,需要满足一条相当特定的攻击路径:
- 某个加密货币相关的 Web 应用的维护者,合并了一个升级了上述依赖的 PR
- 这些恶意依赖被打包进了前端代码(而不是只在服务端使用)
- 构建产物被部署上线
- 真实用户在使用该网站时,恰好发起了加密货币交易
也就是说,如果有人只是接受了一个依赖升级的 PR、在 CI 上跑了一下测试,那目前什么实际的损失都还没发生。
但各方仍在继续分析确认所有受影响的包,情况仍有变数。
恶意代码具体做了什么
反混淆之后的代码逻辑分为两大部分:
第一部分:劫持 HTTP 响应中的加密地址
恶意代码对浏览器原生的 fetch 和 XMLHTTPRequest 进行了 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)的基础设施,在危机处理上的响应效率,显然不尽如人意。
如果你的项目依赖了这些包,该怎么办
第一步:确认影响范围
检查你的项目是否直接或间接依赖了以下包:color、color-string、color-convert、color-name、simple-swizzle、chalk-template、supports-hyperlinks、slice-ansi、wrap-ansi、error-ex、is-arrayish、backslash。
# 用 npm 检查依赖树
npm ls color
# 或者用 yarn
yarn why color
第二步:判断风险等级
- 如果这些包只用于服务端(Node.js 环境),且没有被打包进浏览器端代码:当前没有直接风险,因为恶意载荷是面向浏览器的。
- 如果这些包被打包进了前端代码,且你的应用涉及加密货币相关功能:需要立即排查,查看是否在被投毒的时间窗口内构建并部署了含有恶意代码的版本。
第三步:锁定或升级依赖
确认受影响版本号,升级到已修复的干净版本,或临时锁定到未受影响的旧版本。
第四步:审计已部署的前端资产
如果你在被投毒时间窗口内(约 UTC 13:00 至事后 npm 移除恶意版本之间)重新构建并部署了前端代码,应当:
- 立即重新构建和部署
- 审计这段时间内是否有用户发起了加密货币相关的操作
结语
这次事件是供应链安全问题的又一次教科书级别的演示。攻击者不需要攻破 npm 的服务器,也不需要给每个项目单独注入恶意代码——只需要控制一个高影响力的 npm 账号,就可以把毒素注入到整个依赖树中。
对于普通开发者来说,这件事的教训很简单:
- 收到"请重置 2FA"的邮件,先停下来,去官网自己登录,不要点邮件里的链接。
- 自动化的依赖更新 PR(比如 Dependabot、Renovate)需要认真审查,而不是一看测试绿了就合并。
- 尽可能使用
package-lock.json或yarn.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/
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)