Claude Code源码泄露事件深度解析:从Source Map到npm包还原的完整链路

发布时间:2026/10/4 12:10:41
Claude Code源码泄露事件深度解析:从Source Map到npm包还原的完整链路
1. 一次 npm 发布事故为什么能还原出 51 万行 TypeScriptClaude Code 源码泄露这件事表面看是“一个 60MB 的 .map 文件被误发到 npm”本质却是一次非常典型的前端工程化事故。它值得每个做 TypeScript、React、Node CLI 的开发者认真复盘因为泄露路径并不神秘生产包没有排除 Source Map任何人下载 npm tarball 后都能用脚本把压缩后的 cli.js 还原成接近开发态的源码结构。你如果正在维护私有 npm 包、内部 CLI 工具或者公司有前端构建产物发布流程这篇内容就是写给你的。先把核心概念说清楚。Source Map 是前端构建的标准调试文件它记录压缩代码与原始源码之间的位置映射。正常开发时它让浏览器 DevTools 能把报错定位到 index.ts 的第 12 行但在生产发布场景里如果 .map 文件里带有sourcesContent字段那就等于把原始 TypeScript 源码原封不动塞进了发布包。Claude Code 这次的问题就在于cli.js.map没有被.npmignore或files字段排除直接进了公共 registry。我用一个最小例子帮你建立直觉。假设你写了一个sum.ts// sum.ts export function calculateSum(a: number, b: number): number { return a b; } console.log(calculateSum(100, 200));经过 esbuild 或 webpack 压缩后产物可能变成function calculateSum(n,t){return nt}console.log(calculateSum(100,200));对应的sum.js.map大致长这样{ version: 3, file: sum.js, sources: [../src/sum.ts], sourcesContent: [export function calculateSum(a: number, b: number): number {\n return a b;\n}\nconsole.log(calculateSum(100, 200));], names: [calculateSum, a, b], mappings: AAAA,SAASA,eAAe... }注意sourcesContent这一项。它不是一个“指针”而是完整源码字符串。只要这个字段存在还原脚本甚至不需要去猜变量名直接读出来就是原始文件。Claude Code 的cli.js.map之所以能还原出 1900 多个文件、51 万行代码就是因为构建配置里保留了sourcesContent: true同时发布流程又没有把.map排除掉。这里有个容易被忽略的点很多人以为“我压缩混淆了别人看不懂”。但 Source Map 的存在让混淆几乎失效。变量名n、t会被映射回a、b注释、类型标注、文件目录结构全部回来。对于 React Ink 这种终端 UI 代码还原后连组件层级、Hooks 调用顺序都清清楚楚。从工程视角看这次事故暴露的是三道防线同时失效。第一道是构建配置生产环境应关闭 Source Map或至少设置sourcesContent: false。第二道是发布过滤.npmignore里应写*.map或者用package.json的files白名单只放dist/*.js。第三道是发布前校验npm publish --dry-run会列出待发布文件清单60MB 的 map 文件不可能看不见。三道防线只要有一道生效就不会有这次泄露。我试过在本地复现这个链路结论很直接只要 tarball 里有 map还原成本几乎为零。所以下面我不只讲“事件发生了什么”而是把可复制的检测脚本、解包验证步骤、以及如何用 TaoToken 这类 API 网关做构建产物安全巡检的自动化完整走一遍。你跟着操作能在自己的项目里跑出一份“Source Map 泄露风险报告”。2. 用 TaoToken 搭建源码泄露检测与还原验证环境要复现“从 npm 包还原源码”的完整链路你需要一个能稳定调用大模型来做代码分析和报告生成的入口。这里我用 TaoToken 作为统一 API 网关原因是它兼容 OpenAI 风格的接口配置简单适合把“解包—扫描—还原—分析”串成自动化脚本。TaoToken 本身是一个模型 API 聚合服务你可以把它理解成一个统一的调用入口底层对接多种模型前端用同一套 Base URL 和 Key。先明确你要准备的三件套Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数。API Key 在控制台的 API Keys 页面创建地址是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。Model ID 根据你手头可用的模型填比如做代码分析可以选一个上下文较长的模型。如果你还没决定用哪个模型可以先到模型对话页面试一下效果https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。为什么检测脚本要接大模型因为单纯扫描.map文件只能告诉你“有没有泄露”但还原出来的源码里哪些是敏感逻辑、哪些是内部功能开关、哪些是硬编码的 endpoint这些需要语义理解。把还原后的文件列表和关键片段丢给模型让它输出风险分级比人工翻 1900 个文件高效得多。环境准备分三步。第一步确认 Node 版本不低于 18因为后面用的npm pack和source-map库都依赖较新的 APInode -v npm -v第二步建一个独立的工作目录避免污染你现有项目mkdir -p ~/sourcemap-audit cd ~/sourcemap-audit npm init -y npm install source-map0.7.4 pacote17.0.6这里source-map用来消费 map 文件pacote用来从 registry 拉取 tarball 而不安装依赖。第三步配置 TaoToken 的环境变量不要把 Key 写进代码export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_MODEL你的ModelID如果你用的是 Claude Code 这类终端工具做辅助开发可以把 TaoToken 作为后端接入。Claude Code 的配置通常涉及 Base URL、Key、Model ID 三项写入对应的 settings 文件即可。具体路径和字段参考官方文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。如果你更习惯用 Coding Plan 做长期编码任务也可以走https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak开通。配置完成后先用一个最小请求验证网关连通curl -s $TAOTOKEN_BASE_URL/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: $TAOTOKEN_MODEL, messages: [{role: user, content: 只回复 ok}], max_tokens: 10 }如果返回里有choices字段说明网关通了。这一步很重要因为后面扫描脚本会把分析结果通过这个接口发出去。如果这里报 401先检查 Key 是否复制完整、有没有多余空格如果报 model not found检查 Model ID 是否和你在控制台看到的一致。接下来准备一个“靶场”包。你可以用任意公开的、带 map 的 npm 包做测试但为了安全建议自己构建一个。建一个最小 TS 项目mkdir -p ~/sourcemap-audit/target/src cd ~/sourcemap-audit/target npm init -y npm install typescript esbuild --save-dev写一个src/secret.ts故意放一点“敏感”逻辑// src/secret.ts const INTERNAL_ENDPOINT https://internal.example.com/v1/debug; export function buildDebugUrl(token: string): string { return ${INTERNAL_ENDPOINT}?token${token}; }再用 esbuild 打包并生成带sourcesContent的 mapnpx esbuild src/secret.ts --bundle --minify --sourcemap --outfiledist/secret.js检查dist/secret.js.map里是否有sourcesContentnode -e const mrequire(./dist/secret.js.map); console.log(has sourcesContent:, Array.isArray(m.sourcesContent) m.sourcesContent.length0)输出true就说明你的靶场包已经具备“泄露条件”。这个包就是你后面验证还原脚本的样本。整个环境搭下来大概十分钟但它是后面所有步骤的基础。别跳过因为很多人在真实项目里排查时连“map 里到底有没有 sourcesContent”都没确认过。3. 可复制的 Source Map 提取与还原配置这一节是核心操作。我会给你一份完整的 Node 脚本做三件事从 npm tarball 里找出所有.map文件、解析sourcesContent并还原源码、把还原结果和风险摘要通过 TaoToken 生成报告。脚本可以直接复制运行。先建audit.js// audit.js const fs require(fs); const path require(path); const pacote require(pacote); const { SourceMapConsumer } require(source-map); const PKG process.argv[2] || your-package-name; const VERSION process.argv[3] || latest; const OUT path.resolve(./restored); async function fetchTarball() { const manifest await pacote.manifest(${PKG}${VERSION}); const tarball await pacote.tarball(${PKG}${VERSION}); const tgzPath path.join(process.cwd(), ${PKG.replace(/, _)}.tgz); fs.writeFileSync(tgzPath, tarball); console.log(tarball saved:, tgzPath, size:, tarball.length); return { tgzPath, manifest }; } function extractTarball(tgzPath) { const extractDir path.join(process.cwd(), extracted); fs.mkdirSync(extractDir, { recursive: true }); const { execSync } require(child_process); execSync(tar -xzf ${tgzPath} -C ${extractDir}); return path.join(extractDir, package); } function findMaps(dir) { const results []; function walk(current) { for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full path.join(current, entry.name); if (entry.isDirectory()) walk(full); else if (entry.name.endsWith(.map)) results.push(full); } } walk(dir); return results; } async function restoreFromMap(mapPath) { const raw JSON.parse(fs.readFileSync(mapPath, utf-8)); const files []; if (Array.isArray(raw.sourcesContent)) { raw.sources.forEach((src, i) { const content raw.sourcesContent[i]; if (typeof content string) { files.push({ source: src, content }); } }); } return { mapPath, file: raw.file, count: files.length, files }; } async function main() { const { tgzPath } await fetchTarball(); const pkgDir extractTarball(tgzPath); const maps findMaps(pkgDir); console.log(found map files:, maps.length); fs.mkdirSync(OUT, { recursive: true }); let totalRestored 0; const summary []; for (const mapPath of maps) { const result await restoreFromMap(mapPath); totalRestored result.count; summary.push({ map: path.relative(pkgDir, mapPath), restored: result.count }); for (const f of result.files) { const safeName f.source.replace(/^\.\.\//, ).replace(/[\/\\:]/g, _); fs.writeFileSync(path.join(OUT, safeName), f.content); } } fs.writeFileSync( path.join(OUT, _summary.json), JSON.stringify({ package: PKG, version: VERSION, maps: summary, totalRestored }, null, 2) ); console.log(total restored files:, totalRestored); console.log(output dir:, OUT); } main().catch((e) { console.error(audit failed:, e.message); process.exit(1); });运行方式node audit.js 你的包名 latest脚本逻辑拆开看。pacote.tarball直接从 registry 拉取压缩包不执行安装脚本避免供应链风险。tar -xzf解包后findMaps递归找出所有.map。restoreFromMap读取sourcesContent把每个原始文件写到restored/目录。最后生成_summary.json记录每个 map 还原了多少文件。这里有个关键点不是所有 map 都有sourcesContent。有些构建配置只保留mappings这种情况下还原需要结合压缩后的 JS 和映射关系逐行反解复杂度高很多。但 Claude Code 的情况属于前者sourcesContent直接给了完整源码所以还原几乎是“解压即得”。你可以先用靶场包验证cd ~/sourcemap-audit node audit.js ./target如果restored/里出现了secret.ts且内容完整说明脚本工作正常。接下来把还原结果接入 TaoToken 做风险分析。新增一个analyze.js// analyze.js const fs require(fs); const path require(path); const BASE process.env.TAOTOKEN_BASE_URL; const KEY process.env.TAOTOKEN_API_KEY; const MODEL process.env.TAOTOKEN_MODEL; async function analyze(files) { const sample files.slice(0, 20).map((f) // FILE: ${f}\n${f.slice(0, 800)}).join(\n\n); const body { model: MODEL, messages: [ { role: system, content: 你是代码安全审计助手输出简洁的风险分级。 }, { role: user, content: 以下是还原出的源码片段请指出可能的敏感信息类型endpoint、密钥、内部逻辑并给出风险等级。\n\n${sample} } ], max_tokens: 800 }; const res await fetch(${BASE}/v1/chat/completions, { method: POST, headers: { Authorization: Bearer ${KEY}, Content-Type: application/json }, body: JSON.stringify(body) }); const data await res.json(); if (!data.choices) throw new Error(JSON.stringify(data)); return data.choices[0].message.content; } async function main() { const dir path.resolve(./restored); const files fs.readdirSync(dir) .filter((f) f.endsWith(.ts) || f.endsWith(.tsx) || f.endsWith(.js)) .map((f) fs.readFileSync(path.join(dir, f), utf-8)); const report await analyze(files); fs.writeFileSync(./risk-report.md, report); console.log(report); } main().catch((e) { console.error(e.message); process.exit(1); });运行node analyze.js这一步会把还原出的源码样本发给模型输出风险报告。注意max_tokens和样本数量要控制避免超出上下文。真实项目里你可以按文件类型分批分析比如先看config、auth、api相关文件。如果你用 Claude Code 做日常开发可以把这套脚本挂到 pre-publish 钩子里。Claude Code 的配置三件套Base URL、Key、Model ID写入 settings 后它就能在终端里直接调用模型做分析。具体接入方式看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。如果你需要长期跑这类审计任务Coding Plan 更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。配置片段方面如果你用.npmrc或 CI 环境变量建议这样组织# .taotoken.toml [api] base_url https://taotoken.net/api model 你的ModelID [audit] fail_on_map true max_restored_files 5000这个 TOML 只是示例结构实际字段以你项目约定为准。核心是把fail_on_map true作为发布门禁一旦检测到.map就阻断发布。4. 验证请求与还原结果从 tarball 到源码目录脚本写完后必须验证它真的能跑通而不是“看起来能跑”。这一节我用靶场包走一遍完整流程并给出预期输出。你照着做能确认自己的检测链路是否有效。第一步确认靶场包已经构建好cd ~/sourcemap-audit/target ls -lh dist/你应该看到secret.js和secret.js.map。检查 map 大小du -h dist/secret.js.map如果 map 比 js 还大说明sourcesContent确实把源码塞进去了。这是泄露的典型特征。第二步回到审计目录运行提取脚本cd ~/sourcemap-audit node audit.js ./target预期输出类似tarball saved: .../target.tgz size: 12345 found map files: 1 total restored files: 1 output dir: .../restored注意./target这种本地路径pacote也支持它会按本地包处理。如果你要测公共包直接传包名即可。第三步检查还原结果ls restored/ cat restored/secret.ts你应该看到完整的secret.ts内容包括INTERNAL_ENDPOINT和buildDebugUrl函数。这就证明只要 map 里有sourcesContent还原就是确定性的不需要任何“逆向技巧”。第四步跑风险分析node analyze.js预期输出是一段风险描述指出internal.example.com这类内部 endpoint 属于敏感信息。如果模型返回了choices之外的字段检查 Key 和 Model ID。这一步的验证价值在于它把“文件还原”升级成了“语义风险识别”你能知道泄露的源码里哪些部分最危险。第五步做一个对照实验。修改 esbuild 命令去掉--sourcemapnpx esbuild src/secret.ts --bundle --minify --outfiledist/secret.js重新打包后dist/里没有.map。再跑一次node audit.js ./target预期输出found map files: 0 total restored files: 0这个对照说明关闭 Source Map 生成泄露链路直接断掉。这也是最根本的防护手段。第六步验证.npmignore过滤。在靶场包里建.npmignore*.map dist/*.map然后模拟发布npm pack --dry-run输出里不应该出现.map文件。如果你用package.json的files白名单写法是{ files: [dist/*.js, dist/*.d.ts] }注意白名单里不要写dist整个目录否则 map 会被带进去。要精确到扩展名。第七步把整个流程串成 CI 检查。在package.json里加{ scripts: { audit:map: node audit.js . node -e \const srequire(./restored/_summary.json); if(s.totalRestored0){console.error(Source Map leak detected); process.exit(1)}\ } }这样在npm publish前跑npm run audit:map一旦还原出文件就退出码非零阻断发布。这个门禁比人工检查可靠得多。验证过程中有几个细节要注意。pacote拉取公共包时可能受网络影响如果超时可以加--prefer-offline或换 registry。tar命令在 Windows 上可能不可用可以用tar的 npm 包替代。还原出的文件名做了扁平化处理真实项目里你可能想保留目录结构把safeName改成path.join(OUT, f.source)并提前mkdirSync即可。跑完这一套你手里就有了一份可复用的“Source Map 泄露检测工具”。它不依赖任何特定框架TypeScript、React、Vue、Node CLI 都适用。核心逻辑就一句话找到 map读sourcesContent写文件做语义分析。5. 常见报错排查401、local proxy failed、reading choices 与 OAuth实际跑这套流程时最容易卡在环境配置和接口调用上。这一节我把真实遇到过的报错和排查路径列出来你对照着改。报错一401 Unauthorized{error:{message:Invalid API key,type:invalid_request_error}}原因通常是 Key 复制不完整、带了空格、或者用了错误的 Base URL。排查步骤先echo $TAOTOKEN_API_KEY看有没有值再确认TAOTOKEN_BASE_URL是https://taotoken.net/api注意结尾不要多斜杠。如果 Key 是在控制台新建的确认它没有被禁用。重新生成一个 Key 再试https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。报错二local proxy failed / connection refusedError: connect ECONNREFUSED 127.0.0.1:7890这是本地环境变量里残留了代理设置导致请求被转发到不存在的端口。检查env | grep -i proxy如果有HTTP_PROXY、HTTPS_PROXY、ALL_PROXY临时清掉unset HTTP_PROXY HTTPS_PROXY ALL_PROXY然后重新跑curl验证。注意这里说的是清理本地无效代理配置不是让你去配置任何网络工具。企业内网环境下应该用公司合规的网络出口不要自行改动。报错三reading choices of undefinedTypeError: Cannot read properties of undefined (reading choices)这个报错在analyze.js里出现说明data.choices是 undefined。原因通常是接口返回了错误结构但脚本没检查。改进方式是先打印完整响应const data await res.json(); if (!data.choices) { console.error(unexpected response:, JSON.stringify(data, null, 2)); throw new Error(no choices); }常见触发原因Model ID 写错、max_tokens超过模型上限、请求体 JSON 格式错误。逐一核对。如果返回里是{error:...}按错误信息处理。报错四OAuth token expired / authentication failed如果你用 Claude Code 或其他终端工具接入可能会遇到 OAuth 相关报错。这类工具通常支持两种认证API Key 和 OAuth。用 TaoToken 时应该走 API Key 模式把 Base URL 指向https://taotoken.net/apiKey 填控制台生成的。如果工具里残留了旧的 OAuth 配置先清掉对应的 credentials 文件再重新配置三件套Base URL、Key、Model ID。具体字段名参考文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。报错五tar 解包失败tar: Unrecognized archive format说明pacote.tarball拿到的不是 gzip 流。检查tgzPath文件头file target.tgz正常应该是gzip compressed data。如果不是可能是 registry 返回了 HTML 错误页。打印tarball.length看大小如果只有几百字节基本就是错误响应。检查包名和版本号是否正确。报错六还原出的文件为空restored/目录里文件存在但内容为空。原因是sourcesContent数组里有null。脚本里已经用typeof content string过滤了但如果你的 map 是sourcesContent: [null, ...]需要额外处理。改进if (typeof content string content.length 0) { files.push({ source: src, content }); }同时统计null数量输出到 summary 里方便判断构建配置。报错七npm pack 仍然包含 map你明明写了.npmignore但npm pack --dry-run还是列出.map。原因是package.json里的files字段优先级高于.npmignore。如果files里写了dist那dist下所有文件都会被打包.npmignore不生效。解决方法是把files改成精确白名单或者干脆删掉files字段只用.npmignore。验证方式npm pack --dry-run 21 | grep -i map没有输出才算通过。报错八CI 里 exit code 不生效npm run audit:map在本地能阻断但 CI 里继续跑了。检查 CI 脚本有没有用|| true吞掉退出码或者有没有把npm run放在子 shell 里。正确写法是让失败直接中断- run: npm run audit:map不要加continue-on-error: true。另外确认audit.js在检测到泄露时process.exit(1)。这些报错覆盖了从环境配置到脚本逻辑的主要坑点。排查时记住一个原则先验证最小请求curl 通不通再验证脚本单步解包、找 map、还原最后验证集成CI 门禁。不要一上来就怀疑模型或网关大部分问题出在本地配置和文件路径上。6. 把检测链路固化成发布门禁与长期巡检事件复盘的价值不在于围观而在于把教训变成可执行的流程。Claude Code 这次泄露的核心教训是构建配置、发布过滤、发布前校验三道防线不能只靠人记。你要把它们写成脚本、挂进 CI、做成门禁。具体落地建议分三层。第一层是构建层在生产构建配置里显式关闭 Source Map或者设置sourcesContent: false。以 esbuild 为例npx esbuild src/index.ts --bundle --minify --sourcemapexternal --outfiledist/index.js注意--sourcemapexternal仍然会生成 map 文件只是不内联。真正安全的是不加--sourcemap或者生成后立即删除。webpack 对应devtool: falseVite 对应build.sourcemap: false。如果你需要错误监控可以用hidden-source-map并把 map 上传到私有错误平台但绝不能让它进 npm 包。第二层是发布层用files白名单精确控制发布内容。推荐写法{ files: [ dist/*.js, dist/*.d.ts, README.md, LICENSE ] }不要写dist目录。同时加.npmignore作为双保险*.map *.ts !*.d.ts src/ test/第三层是校验层把第 3 节的audit.js挂到prepublishOnly钩子{ scripts: { prepublishOnly: npm run audit:map } }这样每次npm publish前都会自动扫描。如果检测到 map 且能还原出源码直接阻断。对于 monorepo可以在根目录加一个巡检脚本遍历所有子包for pkg in packages/*; do (cd $pkg node ../../scripts/audit.js . || exit 1) done长期巡检方面建议每周对已发布的包做一次抽查。用pacote拉取线上版本跑一遍还原脚本确认没有 map 泄露。这个任务可以交给 CI 定时触发结果通过 TaoToken 生成摘要报告推送到团队频道。如果你用 Coding Plan 做长期任务编排可以把巡检脚本和报告生成串起来https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。还有一个容易被忽略的点依赖包的 map 泄露。你自己的包干净不代表依赖干净。可以用npm ls列出依赖对每个依赖跑一遍检测。虽然工作量大但可以用脚本批量处理。重点检查那些包含 CLI、构建工具、内部 SDK 的包。对于 AI 生成的构建配置一定要人工复核。现在很多工具能一键生成tsconfig.json、vite.config.ts、webpack.config.js但默认配置未必适合生产发布。比如某些模板默认开启sourcemap: true你如果直接发布就会重演类似问题。把“检查 sourcemap 配置”加入代码评审清单。最后把这次事件的检测脚本沉淀成团队资产。audit.js可以扩展成支持多种输入本地目录、npm 包名、tarball 路径。输出格式可以加 JSON 和 Markdown 两种方便接入不同系统。风险分析部分可以按文件类型分级比如auth、config、api目录下的文件权重更高。如果你想把还原后的源码做进一步分析可以用模型对话页面交互式提问https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。把关键文件贴进去问“这段代码有没有硬编码密钥”“这个 endpoint 是不是内部地址”比人工翻代码快很多。API Key 管理入口在这里https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentsourcemap_leak。整套流程跑下来你会发现防护并不复杂难的是把它变成习惯。Source Map 本身是好东西问题出在“生产发布时忘了排除”。把检测脚本挂进 CI把发布门禁设成硬性阻断这类事故就能从“靠运气”变成“靠流程”。