![]()
2025年初,一家知名AI公司的CLI工具上线npm后,社区用户用30秒完成了整件事——不是下载使用,而是把他们的TypeScript源码、系统提示词、内部逻辑全部提取完毕。团队发现时,代码已经在GitHub上被逐行分析。
问题出在source map(源映射)文件。这些.json文件本该只在调试时出现,却跟着构建产物一起被打包发布。更讽刺的是,开发者可能从未在仓库里见过它们——因为dist/目录通常被.gitignore排除,只在构建后短暂存在,然后直达npm服务器。
source map的本质是一份"翻译词典":把压缩后的代码映射回原始位置,方便你在浏览器或Node.js里调试。
但大多数人没意识到,这份词典里夹着完整的原文。sourcesContent字段是一个字符串数组,里面是你的每一行原始代码,一字不差。攻击者不需要反编译,不需要逆向工程,JSON.parse()就能读完你的商业秘密。
本文用事件还原的方式,拆解这条泄露链的每个节点,以及堵住它的三重保险。
泄露现场:一份JSON里的全部家底
source map文件的内部结构长这样:
version字段标记格式版本,sources列出原始文件路径,mappings是编码后的位置映射——而sourcesContent,就是你的完整源码。
这意味着什么?任何人安装你的npm包后,在node_modules里找到.map文件,就能读到你在src/auth/middleware.ts里写的注释、在src/config/secrets.ts里硬编码的API端点、甚至你随手写的TODO和FIXME。
那家AI公司的泄露事件之所以迅速发酵,正是因为他们的系统提示词(system prompts)也被编进了源码。这些本应对用户隐藏的指令,成了社区研究的标本。
技术社区的反应分成两派。一部分人觉得这是低级失误:"发布前不检查files字段?"另一部分人则感到后怕——因为他们检查了自己的包,发现同样的问题。
npm的下载统计给出了更广泛的图景。2024年,npm日均下载量超过200亿次,其中相当比例的包包含source map文件。大多数是无意的,但"无意"不等于无害。
构建 pipeline 的盲区:第四步埋雷
![]()
典型的前端构建流程有四步:TypeScript编译、代码压缩、生成source map、npm发布。危险藏在第四步的默认行为里。
npm publish的默认策略是"几乎全发"——只要文件不在.gitignore里,就会被打包。
而source map文件通常躺在dist/目录,这个目录又往往被.gitignore排除(因为构建产物不该进版本控制)。于是开发者陷入盲区:仓库里看不见这些文件,构建时它们却凭空出现,然后被npm一并收走。
这种设计像什么?像你家装修完,工人把备用钥匙藏在门口地垫下,你自己都不知道。
更隐蔽的风险在于嵌套依赖。你的包可能依赖了另一个泄露源码的包,而用户安装时,这些.map文件会层层传递。你检查了自己的发布清单,却没检查transitive dependencies(传递依赖)的清单。
一些团队尝试用postbuild脚本自动删除.map文件,但脚本可能失败、可能被跳过、可能在CI环境里行为不一致。依赖"事后清理"不如在源头阻断。
三重保险:从允许列表到双重验证
堵住泄露需要多层防御,任何单层都可能失效。
第一重:package.json里的files字段。
这是最有效的手段。不要试图排除坏文件,而是明确列出要发布的文件:
注意列表里有什么:编译后的.js文件、类型声明.d.ts、文档和许可证。注意没有什么:*.map文件、src/源码目录、测试文件、配置文件。
这是允许列表(allowlist)思维,比黑名单(blocklist)更安全——因为你无法预测未来会出现什么新文件类型,但你可以确定哪些是必须发布的。
第二重:.npmignore文件。
![]()
即使有了files字段,再加一层防护。一个典型的.npmignore长这样:
这里有个陷阱:如果.npmignore存在,npm会完全忽略.gitignore。这意味着你不能假设"反正.gitignore排除了dist/,npm也不会发"——.npmignore的存在切断了这条继承链。
有些团队把.npmignore当成.gitignore的复制粘贴,这是错的。两者的语义不同:.gitignore管版本控制,.npmignore管发布行为。你需要分别维护,或者干脆只用files字段做精确控制。
第三重:发布前的自动化检查。
在CI流程里加一步:运行npm pack --dry-run,检查生成的tarball内容。或者使用工具如publint,它会扫描你的包并标记常见问题,包括意外的source map文件。
更激进的方案是在构建阶段禁用source map生成。对库(library)来说,用户不需要调试你的内部实现;对应用(application)来说,source map应该走单独的发布渠道,绝不混在npm包里。
但这里有个权衡。禁用source map会让你的崩溃日志失去可读性,错误追踪服务(如Sentry)需要它们来还原堆栈。解决方案是:生成source map,但不上传npm——通过Sentry CLI等工具单独上传,或者托管在私有存储桶。
行业余震:从个案到系统性反思
那家AI公司的泄露事件后,npm生态出现了几个连锁反应。
一些安全扫描工具开始把source map检测加入默认规则。GitHub的Dependabot、Snyk、Socket.dev都能标记包含敏感内容的.map文件。但这属于事后发现,不是事前预防。
TypeScript和构建工具也在调整默认行为。Vite 6.0的库模式默认不再生成source map,除非显式开启。Rollup的@rollup/plugin-typescript增加了发布检查警告。这些改动很慢,因为涉及 breaking change(破坏性变更),但方向是明确的。
更深层的问题是:为什么这么多专业团队会犯这个错误?
一个解释是"抽象泄漏"。现代前端工具链把构建过程包得太好,开发者习惯了npm run build && npm publish,却不清楚中间产物是什么。source map是"透明"的基础设施,直到它变得不透明。
另一个解释是测试与生产的鸿沟。本地开发时source map有用,团队假设发布流程会"自动处理"它们。但npm的发布模型是"默认开放",这个假设本身就是错的。
那家AI公司在事件后的回应很有代表性:他们没有公开道歉,而是默默更新了构建流程,在文档里加了一节"安全发布指南"。社区有人觉得这是"冷处理",也有人觉得这是"务实"——毕竟,承认失误会吸引更多攻击者来审计他们的其他包。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.