ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

突发!51.2万行代码全网疯传,Claude Code源码泄露事件全复盘:从source map到npm包的完整链路拆解

突发!51.2万行代码全网疯传,Claude Code源码泄露事件全复盘:从source map到npm包的完整链路拆解 1. 从一次 npm 安装说起source map 是怎么把 51.2 万行代码端上桌的你可能已经在各种群里看到那张截图了一个 57MB 的cli.js.map文件静静躺在 npm 包的发布目录里顺着它就能把 1900 多个 TypeScript 源文件、51.2 万行代码完整还原出来。这件事之所以炸裂不是因为有人攻破了什么防线而是因为发布流程里少了一行过滤规则。我试过用同样的思路去审查自己项目里的构建产物发现这类问题离普通开发者其实一点都不远。先把概念说清楚不然后面的命令你会看得云里雾里。source map 本质上是一张“翻译对照表”。你写的 TypeScript 代码经过打包工具压缩、混淆、去掉注释和类型之后变成体积更小、可读性极差的 JavaScript。浏览器或 Node 运行时执行的是这份压缩后的代码一旦线上报错堆栈里全是a.b.c is not a function这种鬼东西根本没法定位。source map 就是用来把压缩后的位置反向映射回原始 TS 文件、原始行号和原始变量名的。它里面通常包含sources源文件路径列表、mappings位置映射串以及最关键的sourcesContent——原始源码的完整文本。问题就出在sourcesContent上。很多打包工具的默认配置会把原始源码内容直接内联进 map 文件这样调试时不需要额外去服务器拉源文件。方便是真方便但一旦这个 map 文件被发布到公共 npm 仓库等于你把整个项目的源码连同目录结构一起打包送出去了。任何人npm install之后在node_modules里翻出这个 map用现成工具几秒钟就能还原出可读的 TS 工程。这次 Claude Code 的情况更极端一点。泄露的 map 文件里不仅内联了源码还写入了存放完整源码压缩包的存储桶公网地址。也就是说哪怕你不懂 source map 还原只要打开这个文件搜一下链接就能直接下载整包源码。门槛低到几乎为零这才是它能在几小时内席卷全球开发者社区的原因。那为什么打包工具会默认把源码塞进去因为大多数工具的默认假设是source map 只在开发环境用生产环境你会主动关掉或者不发布。但现实是很多团队用 CI 自动发布构建命令和发布命令是两条独立的流水线构建时开了 source map发布时忘了在.npmignore或files字段里排除*.map于是就发出去了。Claude Code 这次踩的就是这个坑。对普通开发者来说这件事的实用价值不在于吃瓜而在于它给了你一套可复用的自查方法怎么确认自己的 npm 包里有没有混进 source map怎么用命令还原一份 map 看看里面到底有什么以及怎么在 CI 里加一道自动拦截。下面我会把这几步拆开配上可以直接复制的命令和配置。你不需要有安全背景只要能跑 npm 和 node 就能跟着做。顺带说一句如果你平时用 Claude Code 这类 AI 编程工具做日常开发工具本身的源码泄露和你的使用是两回事你的账号和对话数据不受影响。但如果你想把这类工具接进自己的工程流或者想用更可控的方式调用模型后面我会给出一套标准的接入配置把 Base URL、Key、Model ID 三件套讲清楚避免你在环境变量和配置文件之间来回试错。2. 复现泄露链路用 source-map 工具还原一份被发布的 map 文件要理解这次事件最好的方式是自己动手还原一次。你不需要去下载泄露的源码包随便找一个自己项目里构建出来的 map 文件或者用打包工具生成一个测试用的就能完整走一遍链路。这一节的目标是让你掌握三个能力识别 map 文件、还原源码、判断泄露范围。先准备一个测试环境。新建一个目录初始化一个最小的 TypeScript 项目mkdir sm-test cd sm-test npm init -y npm install typescript esbuild --save-dev写一个简单的 TS 文件src/index.tsinterface User { id: number; name: string; } export function greet(user: User): string { return hello ${user.name}, your id is ${user.id}; } console.log(greet({ id: 1, name: tester }));用 esbuild 构建并且显式开启 source mapnpx esbuild src/index.ts --bundle --outfiledist/cli.js --sourcemap --minify执行完你会看到dist/下多了两个文件cli.js和cli.js.map。打开cli.js内容是被压缩混淆过的变量名变成了单字母。打开cli.js.map它是一个 JSON里面有几个关键字段{ version: 3, sources: [../src/index.ts], sourcesContent: [interface User {\n id: number;\n name: string;\n}\n\nexport function greet...], mappings: AAAA,SAAS... }看到sourcesContent了吗原始 TS 源码一字不差地躺在里面。这就是泄露的物理载体。现在用工具把它还原出来。安装source-map这个库npm install source-map --save-dev写一个还原脚本restore.jsconst fs require(fs); const { SourceMapConsumer } require(source-map); async function restore(mapPath, outDir) { const raw JSON.parse(fs.readFileSync(mapPath, utf8)); const consumer await new SourceMapConsumer(raw); consumer.sources.forEach((source, idx) { const content consumer.sourceContentFor(source, true); if (!content) return; const safeName source.replace(/\.\.\//g, ).replace(/\//g, __); const outPath ${outDir}/${safeName}; fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(outPath, content); console.log(restored: ${source} - ${outPath} (${content.split(\n).length} lines)); }); consumer.destroy(); } restore(process.argv[2], process.argv[3] || ./restored);运行node restore.js dist/cli.js.map ./restored你会看到终端打印出还原的文件路径和行数restored/目录下就是完整的原始 TS 源码。整个过程没有任何反编译、反混淆纯粹是读取 map 里已经存好的内容。这就是为什么这次事件被称为“零门槛泄露”。现在把视角放大到 npm 包审查。假设你想检查一个已发布的包有没有混进 map 文件不需要安装它直接用npm pack把 tarball 拉下来看内容npm pack claude-code2.1.88 --dry-run--dry-run会列出这个包会包含哪些文件不会真的下载。如果你看到列表里有.map结尾的文件尤其是体积很大的就要警惕了。想进一步确认可以真正下载并解包npm pack claude-code2.1.88 tar -tzf claude-code-2.1.88.tgz | grep -E \.map$tar -tzf列出压缩包内所有文件配合grep过滤出 map 文件。如果输出里有cli.js.map这种基本可以确认源码有暴露风险。再进一步把包解开找到 map 文件用上面的restore.js跑一遍就能知道泄露了多少行、哪些目录。判断泄露范围时重点看三个指标map 文件体积、sources数组长度、sourcesContent是否非空。体积几十 MB 的 map通常对应上千个源文件sources长度直接告诉你涉及多少个文件sourcesContent如果全是 null说明只存了映射没存源码风险相对小但只要有一个非空就能还原出对应文件。这套流程不只适用于这次事件。任何你依赖的 npm 包都可以用同样的方式做一次“源码暴露体检”。尤其是那些闭源商业工具如果发布包里带了完整 map等于把实现细节公开了。对使用者来说这未必是坏事但对你自己的项目来说发布前一定要确认 map 没被带出去。3. 可复制配置在 npm 发布流程里彻底堵住 source map 泄露上一节我们验证了泄露是怎么发生的这一节解决“怎么防”。核心思路只有一句话让构建产物和发布产物分离发布包里永远不包含.map文件。听起来简单但实际项目里容易在几个地方翻车我把每个环节的配置都写出来你照着改就行。第一道防线是.npmignore。npm 发布时会读取这个文件把匹配的文件排除掉。很多人以为有.gitignore就够了其实 npm 用的是独立的忽略规则。在项目根目录创建或编辑.npmignore# 排除所有 source map *.map **/*.map # 排除构建中间产物 dist/**/*.ts src/ tsconfig.json .eslintrc* # 排除测试和文档源文件 __tests__/ *.test.ts docs/注意**/*.map这一行它能匹配任意层级的 map 文件比只写*.map更保险。如果你用的是files字段白名单模式推荐逻辑反过来只声明要发布的文件{ name: your-package, version: 1.0.0, files: [ dist/**/*.js, dist/**/*.d.ts, !dist/**/*.map ] }files白名单比.npmignore黑名单更安全因为默认不发布任何未声明的文件。但要注意files里的排除规则!dist/**/*.map必须放在包含规则之后顺序错了不生效。第二道防线是构建配置。以 esbuild 为例生产构建时直接关掉 source mapnpx esbuild src/index.ts --bundle --outfiledist/cli.js --minify --sourcemapfalse如果你用 tsup在tsup.config.ts里区分环境import { defineConfig } from tsup; export default defineConfig({ entry: [src/index.ts], format: [cjs, esm], minify: true, sourcemap: process.env.NODE_ENV ! production, clean: true, });用 webpack 的话在webpack.config.js里把devtool设为falsemodule.exports { mode: production, devtool: false, // ...其他配置 };关键点是开发环境可以开 source map 方便调试生产构建必须关掉或者至少不把 map 发布出去。两者选其一不要既开着 map 又发布出去。第三道防线是 CI 自动拦截。人总会忘所以要在流水线里加一道检查。在package.json里加一个prepublishOnly脚本{ scripts: { prepublishOnly: node scripts/check-no-sourcemap.js } }scripts/check-no-sourcemap.js内容const { execSync } require(child_process); const output execSync(npm pack --dry-run --json, { encoding: utf8 }); const files JSON.parse(output)[0].files.map((f) f.path); const maps files.filter((f) f.endsWith(.map)); if (maps.length 0) { console.error(发布包中发现 source map 文件已阻止发布); maps.forEach((m) console.error( - ${m})); process.exit(1); } console.log(检查通过共 ${files.length} 个文件无 source map。);这个脚本在npm publish之前自动运行一旦发现 map 文件就直接退出发布中断。npm pack --dry-run --json会输出一个 JSON列出将要打包的所有文件解析后过滤.map后缀即可。这套逻辑可以原样搬进 GitHub Actions 或任何 CI 系统。第四道防线是发布后的巡检。即使发布时检查过也建议定期扫描已发布的包。写一个简单的巡检脚本#!/bin/bash PKG$1 VERSION$2 npm pack $PKG$VERSION /dev/null 21 TARBALL$(ls -t *.tgz | head -1) echo 检查 $TARBALL tar -tzf $TARBALL | grep -E \.map$ echo 警告发现 map 文件 || echo 安全无 map 文件 rm -f $TARBALL用法./scan.sh your-package 1.0.0。它会下载指定版本的 tarball列出内容并检查 map 文件最后清理临时文件。把这个脚本挂到定时任务里每周跑一次能及时发现意外发布。如果你在项目里用 Claude Code 这类工具做辅助开发建议把上面的检查脚本也接进你的工作流。工具本身怎么接入是另一回事但发布安全这条线必须自己守住。关于模型接入的配置后面第五节会给出完整的 Base URL、Key、Model ID 三件套写法这里先把发布侧的坑填完。最后提醒一个容易忽略的点有些打包工具会生成.map文件但不在files字段里声明这种情况下 npm 默认不会发布它看起来安全。但如果你的.npmignore写错了或者用了files: [dist]这种粗粒度声明map 就可能被顺带带出去。所以最稳的做法是白名单精确到扩展名再叠加 CI 检查双保险。4. 验证请求与成功结果用命令确认你的包到底暴露了什么配置改完之后你需要一套验证方法确认发布包里确实没有 map同时确认即使有 map 也无法还原出敏感内容。这一节给出一组可以直接跑的命令每一步都有明确的预期输出你对照着看就知道自己有没有做对。第一步本地构建后检查产物目录。构建完成后先看dist/下有没有.map文件find dist -name *.map -type f如果输出为空说明构建阶段就没生成 map这是最理想的情况。如果有输出比如dist/cli.js.map说明构建配置里 source map 还开着回到上一节把sourcemap设为false。第二步用npm pack --dry-run模拟发布看最终会打包哪些文件npm pack --dry-run输出会列出所有将被包含的文件类似npm notice Tarball Contents npm notice 1.2kB dist/index.js npm notice 0.8kB dist/index.d.ts npm notice 0.5kB package.json npm notice 0.3kB README.md重点检查这个列表里有没有.map结尾的文件。如果有说明.npmignore或files配置没生效。注意--dry-run不会真的生成 tarball只是模拟所以可以放心反复跑。第三步真正打包并解压检查。这一步比 dry-run 更可靠因为它走的是完整的打包流程npm pack tar -tzf *.tgztar -tzf会列出压缩包内所有文件的完整路径。预期输出里应该只有.js、.d.ts、package.json、README.md这类文件没有任何.map。如果看到 map 文件说明前面的配置有遗漏。第四步假设你确实发现了一个 map 文件可能是历史版本或者别人的包用还原脚本确认它暴露了什么。复用第二节的restore.jstar -xzf *.tgz node restore.js package/dist/cli.js.map ./leaked ls -la ./leaked wc -l ./leaked/*wc -l会统计还原出来的每个文件的行数加总就是泄露的总行数。如果这个数字达到几十万说明整个项目的源码都暴露了。同时看leaked/目录下的文件结构如果能看到src/、internal/、config/这类目录说明连内部实现细节都还原出来了。第五步检查 map 文件里有没有硬编码的敏感信息。这一步经常被忽略但危害很大。用grep搜索常见敏感关键词grep -oE https?://[^\] package/dist/cli.js.map | sort -u | head -20 grep -iE (secret|token|api[_-]?key|password|bucket) package/dist/cli.js.map | head -20第一条命令提取 map 里出现的所有 URL第二条搜索敏感词。如果输出里出现了存储桶地址、内部 API 域名、或者疑似密钥的字符串说明泄露的不只是源码还有基础设施信息。这次事件里那个存储桶公网地址就是这么被发现的。第六步验证修复后的包。改完配置重新构建、重新打包再跑一遍上面的检查rm -rf dist node_modules/.cache npm run build npm pack --dry-run | grep -c \.map$最后这条命令统计 dry-run 输出里 map 文件的行数。预期输出是0。如果是0说明修复生效如果是其他数字说明还有 map 文件会被发布。把这一套命令串成一个脚本每次发布前跑一遍就能形成稳定的检查习惯#!/bin/bash set -e echo 构建 npm run build echo 检查产物 if find dist -name *.map | grep -q .; then echo 失败dist 中存在 map 文件 exit 1 fi echo 检查发布包 COUNT$(npm pack --dry-run 21 | grep -c \.map$ || true) if [ $COUNT -ne 0 ]; then echo 失败发布包中存在 $COUNT 个 map 文件 exit 1 fi echo 通过无 source map 泄露风险这个脚本可以直接放进package.json的prepublishOnly也可以作为 CI 的一个步骤。跑通之后你会看到最后一行输出“通过无 source map 泄露风险”这就是我们要的成功结果。如果你在验证过程中发现自己的包确实有 map 泄露先别慌。已经发布的版本无法撤回但你可以立即发布一个修复版本同时在 README 或 release notes 里说明情况。对于已经泄露的源码评估一下里面有没有密钥、内部地址、未公开的功能规划如果有尽快轮换密钥、下线相关服务。这次事件里 Anthropic 的做法就是紧急更新 npm 包、删除问题版本、关停存储桶公开访问这套应急流程你可以参考。5. 常见报错排查401、local proxy failed、reading choices 到底卡在哪配置和验证都跑通之后实际使用中还是会遇到各种报错。这一节把最常见的几类错误列出来每个都给出原因和修复方法。这些报错不只出现在源码审查场景你在接入模型 API、配置开发工具时同样会碰到所以值得单独讲清楚。401 Unauthorized。这是最典型的鉴权失败。表现是请求返回 401响应体里通常有invalid_api_key或authentication_error。原因有三个Key 写错了、Key 过期了、或者 Base URL 和 Key 不匹配。排查顺序是先确认 Key 有没有多余空格或换行很多复制粘贴会带上不可见字符。然后确认 Base URL 和 Key 是同一套体系不要拿 A 平台的 Key 去请求 B 平台的地址。修复方法是重新生成 Key用环境变量注入而不是硬编码export TAOTOKEN_API_KEYsk-你的实际key然后在代码里读取process.env.TAOTOKEN_API_KEY。如果你用的是配置文件确保格式正确比如 Claude Code 的 settings 里{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的实际key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }注意 Base URL 末尾不要多加/v1具体路径以接入文档为准。Key 和 Model ID 必须同时配置缺一个都会报错。local proxy failed。这个报错通常出现在你配置了本地代理或者中间层转发的时候。表现是连接被拒绝、超时或者提示ECONNREFUSED。原因可能是代理进程没启动、端口被占用、或者代理配置指向了一个不存在的地址。排查步骤先确认代理进程在跑ps aux | grep proxy看有没有相关进程再确认端口监听lsof -i :端口号最后检查配置文件里的地址和端口是否和实际一致。如果你没有主动配置代理检查一下环境变量里有没有残留的HTTP_PROXY、HTTPS_PROXY这些会干扰正常请求env | grep -i proxy如果有输出且不是你想要的用unset HTTP_PROXY HTTPS_PROXY清掉。修复后重新发起请求确认返回正常。reading choices。这个报错一般出现在解析模型响应的时候提示读取choices字段失败。原因是响应格式和预期不符可能是接口返回了错误信息而不是正常的 completion 结构也可能是你用的 SDK 版本和接口版本不匹配。排查方法先把原始响应打印出来看const resp await fetch(url, options); const text await resp.text(); console.log(status:, resp.status); console.log(body:, text);如果 body 里是{error: {...}}说明请求本身失败了先解决错误信息里的问题。如果 body 是正常的 JSON 但没有choices检查你用的接口是不是 chat completions 格式有些接口返回的是content数组而不是choices。修复方法是根据实际响应结构调整解析逻辑或者升级 SDK 到匹配版本。OAuth 相关报错。如果你用 Claude Code 的 OAuth 登录方式可能会遇到 token 过期、刷新失败、或者invalid_grant。这类问题的根源通常是系统时间不准、token 被撤销、或者回调地址不匹配。先检查系统时间date如果时间偏差超过几分钟OAuth 的签名校验会失败用ntpdate或系统设置同步时间。然后确认你的账号没有被登出重新走一遍授权流程。如果还是失败检查配置文件里的oauth相关字段确保client_id、redirect_uri和注册时一致。模型不存在或 model not found。这个报错说明 Model ID 写错了或者你的账号没有该模型的权限。先确认 Model ID 拼写注意大小写和版本号后缀。然后确认你的套餐是否包含该模型。修复方法是换成你有权限的模型或者升级套餐。配置里三个字段必须同时正确{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的实际key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }Base URL 决定请求发到哪里Key 决定你是谁Model ID 决定用哪个模型。三者缺一不可任何一个错了都会报错。连接超时或 timeout。表现是请求长时间无响应最后超时。原因可能是网络不通、地址写错、或者服务端限流。先用curl直接测一下地址通不通curl -I https://taotoken.net/api如果连不上检查网络和地址。如果能连上但请求超时可能是请求体太大或者模型响应慢适当增加超时时间或者把大请求拆小。另外确认没有把 Base URL 写成带路径的形式导致路由错误。返回内容为空或截断。有时候请求成功但返回内容不完整可能是max_tokens设得太小或者流式响应没处理完。检查请求参数里的max_tokens适当调大。如果是流式响应确保正确处理了data:前缀和[DONE]结束标记。把这几类报错整理成一张对照表方便你快速定位报错关键词最可能原因第一步排查401 UnauthorizedKey 错误或过期检查 Key 和环境变量local proxy failed代理未启动或配置错误检查代理进程和端口reading choices响应格式不符打印原始响应体OAuth invalid_grant时间偏差或 token 撤销同步系统时间重新授权model not foundModel ID 错误核对模型名称和权限timeout网络或地址问题用 curl 测试连通性遇到报错时先看错误信息里的关键词对照这张表定位方向再按上面的步骤逐一排查。大部分问题都能在几分钟内解决。如果排查后仍然无法解决把完整的请求命令、响应状态码和响应体整理好再去查接入文档或提工单这样能大幅提高解决效率。6. 把源码审查和模型接入串起来一套可长期用的工程习惯源码泄露这件事看起来离你很远但它暴露的问题离每个开发者都很近构建产物和发布产物的边界没有管好。这次是 Claude Code下次可能是你正在用的某个依赖包也可能是你自己发布的工具。把前面几节的命令和配置固化成习惯比记住这次事件的细节更有价值。我自己的做法是在每个项目里放一个scripts/目录里面固定三个脚本check-no-sourcemap.js负责发布前拦截scan-published.sh负责定期巡检已发布版本restore-map.js负责在需要时还原 map 内容做审计。这三个脚本加起来不到一百行但能覆盖从构建到发布再到事后检查的完整链路。prepublishOnly钩子挂上第一个CI 里跑第二个第三个只在排查问题时手动用。对于模型接入这部分核心就是把 Base URL、Key、Model ID 三件套配对。不管你用的是 Claude Code、Cline 还是其他工具配置逻辑都一样地址决定请求发往哪里Key 决定身份Model ID 决定能力。三者写在同一个配置文件里用环境变量注入敏感值不要硬编码。如果你需要长期做编码类任务或者跑 Agent 工作流可以考虑用 Coding Plan 这类方案把调用配额和模型选择统一管理避免每次手动切换配置。验证模型是否可用最直接的方式是发一个最小请求。用curl测一下curl https://taotoken.net/api/v1/messages \ -H x-api-key: $TAOTOKEN_API_KEY \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d { model: claude-sonnet-4-20250514, max_tokens: 64, messages: [{role: user, content: ping}] }如果返回正常的 JSON 响应说明配置正确。如果报错回到上一节的排查表定位。这个命令可以直接复制到终端跑把 Key 换成你自己的即可。源码审查这边养成一个习惯每次npm install一个新依赖后花十秒钟看一眼它的发布包里有没有 map 文件。命令很简单npm pack 包名 --dry-run 21 | grep \.map$没有输出就是安全的。有输出的话再决定要不要继续用这个包或者至少知道它的实现是公开的。这个动作成本极低但能让你对自己的依赖链有更清晰的认知。最后说回这次事件本身。51.2 万行代码的泄露根源是一个.npmignore配置疏漏。它提醒我们的不是“大厂也会犯错”而是“基础工程规范的价值被严重低估了”。模型能力再强、架构设计再精妙发布流程里少一行过滤规则所有努力都可能白费。反过来把这一行规则加好把检查脚本挂上你就能避开绝大多数同类问题。如果你想把上面这些检查自动化可以从最小的那一步开始在package.json里加上prepublishOnly脚本把check-no-sourcemap.js跑起来。跑通一次之后再逐步加上巡检和还原审计。不需要一次性搭完整套体系先让发布流程有一道拦截就已经比大多数项目安全了。
返回列表