一名开发者最近披露了一次很典型、也很反常的遭遇。

他在 LinkedIn 上收到一名“招聘者”的私信。对方自称来自一家小型加密创业公司,沟通数日后发来一个公开 GitHub 仓库,请他检查所谓的“deprecated Node modules issue”。

这听起来像一次普通技术面试。问题在于,对方真正想推动的动作,很可能不是代码审查,而是让他运行 npm install

原作者没有在本机直接安装依赖。他开了一台一次性 Hetzner VPS,又用只读代码审查 agent 看项目,才发现仓库里藏着一个会在安装阶段触发的 Node.js 后门。

我更在意的是这条链路:招聘沟通建立信任,GitHub 仓库制造任务,npm 生命周期脚本负责执行。它把开发者每天都会做的动作,改造成了供应链攻击入口。

发生了什么:一个“修旧模块”的任务,指向 npm install

这个仓库外观看起来像一个 React 前端加 Node 后端项目。诱饵也很自然:让候选人检查旧 Node 模块问题。

对开发者来说,这类任务不陌生。拿到仓库,clone,install,run,再看报错。很多面试、外包和远程协作都会这么走。

恶意代码藏在 app/test/index.js。它伪装成测试文件,代码约 250 行。里面把 protocolsubdomaindomainpathtoken 等片段拼起来,形成这个地址:

https://rest-icon-handler.store/icons/77

随后,它会执行远程服务器返回的内容。

真正危险的是触发方式。app/index.js 引入了 ./test,也就会加载 app/test/index.js。而 package.json 里的 prepare 脚本,会在 npm install 后自动运行 app:preapp:pre 执行的是:

node app/index.js

链条并不复杂,但很有效。

环节表面动作实际风险该看的点
LinkedIn 私信技术岗位邀约建立招聘语境下的信任身份是否可验证
GitHub 仓库检查 deprecated Node modules issue推动候选人进入安装流程是否必须本机运行
app/test/index.js测试文件拼接远程 URL 并执行返回内容测试目录不等于安全目录
npm prepare生命周期脚本npm install 后自动触发scripts 是否有自动执行

这也是这起事件比普通钓鱼邮件更麻烦的地方。它不要求你点一个离谱链接,也不要求你下载一个可疑附件。它只是让你做一件“看起来像开发工作”的事。

原作者没有执行远程第二阶段载荷。所以目前不能判断它最终会窃取什么、安装什么,也不能推断攻击者身份、组织背景或受害者规模。

能确认的边界是:这个仓库具备远程代码执行意图,触发点放在 npm 安装流程里。

为什么重要:身份伪装让攻击更像正常协作

这起事件的另一个关键点,是身份。

仓库提交记录显示,39 次提交都归于一名真实全栈开发者的姓名和邮箱。原作者联系该开发者后,对方否认参与,并称自己此前也遭遇过 GitHub 身份冒用,正在举报相关仓库。

LinkedIn 上的“招聘者”身份也可疑。资料属于一名真实艺术记者,背景与技术招聘不匹配。更反常的是,当原作者表示项目装不上时,对方很快切换成熟悉 npm 和 Node 版本的语气,并推动他确认是否运行了 npm install

这里不能说 LinkedIn 或 GitHub 被整体入侵。更稳妥的判断是:真实人物的资料和开发者身份被冒用,用来给恶意仓库补信用。

这和 npm typosquatting、dependency confusion 有相似处,也有差别。

传统供应链攻击常把恶意包放进公共注册表,等开发者或 CI 系统误装。2021 年安全研究员 Alex Birsan 披露的 dependency confusion,就让很多团队重新检查包管理边界。

这次的路径更靠前。恶意代码不是等人搜到,而是被“招聘流程”送到目标面前。边界从包管理器,前移到了 LinkedIn 私信、候选人任务和远程面试环节。

这对两类人影响最直接。

软件开发者要把“陌生仓库”当作不可信输入处理,尤其是自由职业者、远程候选人、开源维护者和正在找工作的资深工程师。现实里,越想快速证明能力,越容易直接 installrun

技术招聘与安全团队也要调整流程。公司如果要求候选人运行项目,就该提供可验证的公司域名邮箱、招聘页面和沙箱说明。不要把不透明脚本丢给候选人的个人设备。

该怎么防:少信头像,多看 scripts

最低限度的防护,不需要复杂工具。

开发者拿到陌生仓库后,先别在主力机器上跑。先看 package.json,尤其是这些 scripts:preinstallinstallpostinstallprepare

prepare 很容易被忽视。它常用于包发布和构建准备,但也会在 npm install 后触发。攻击者把执行链放在这里,就是利用开发者对安装流程的习惯性信任。

更稳的做法是:

  • 用一次性容器、虚拟机或临时 VPS 跑不可信项目。
  • 必要时使用 npm install --ignore-scripts
  • 代码审查先读 scripts 和入口文件,再考虑安装依赖。
  • GitHub 提交身份和 LinkedIn 身份分开核验。
  • 不把头像、姓名、历史提交记录当成充分证明。

招聘团队也有动作要做。

如果面试题必须运行项目,最好提供干净的沙箱、明确说明依赖安装会触发什么脚本,并用公司可验证渠道发送任务。候选人拒绝在个人电脑上运行未知脚本,不该被视为“不配合”。这反而是安全意识。

安全团队接下来该盯的不是单个域名是否下线,而是同类模式是否批量出现:相似的 npm 生命周期脚本、相似的测试文件伪装、相似的身份冒用、相似的 LinkedIn 招聘话术。

原作者称已向 GitHub 和 LinkedIn 举报。但披露时仓库仍在线。平台处置速度,会直接影响这类攻击能扩散多久。

回到开头那个“deprecated Node modules issue”。它看起来像一个小修小补的技术任务,真正的门却开在 npm install 后面。

开发者不可能拒绝所有陌生仓库,也不可能停止面试和远程协作。能做的是把默认动作改掉:先隔离,再安装;先看 scripts,再信任务。