资讯动态

Claude Code 源代码泄露背后:从 npm source map 看 TypeScript 工程安全

发布时间:2026/9/29 22:45:47 来源:尧图企业网站定制
1. 一次 npm 发布事故为什么值得每个 TypeScript 团队复盘Claude Code 源代码泄露这件事表面看是 Anthropic 的发布流程翻车实际暴露的是 TypeScript 工程里一个非常普遍的盲区source map 到底该不该进生产包。安全研究员在 npm 注册表上发现 Claude Code 2.1.88 版本附带了一个 59.8MB 的.map文件这个文件能把压缩混淆后的产物完整映射回原始 TypeScript 源码1900 多个源文件、51.2 万行代码连同手写注释一起暴露在公共互联网上。还原出来的内容包含长达 4.6 万行的 QueryEngine.ts、约 2.9 万行的工具系统定义、40 多个独立工具模块以及一套六级权限验证系统。这件事的技术原因并不复杂Claude Code 使用 Bun 作为运行时和打包工具Bun 的构建器默认生成 source map 文件正常发布流程中这类文件应该通过.npmignore或构建配置被排除但这一次它随正式包一同上传到了 npm 公共注册表。更值得注意的是2025 年 2 月 Claude Code 的一个早期版本就因为同样的问题被曝光过一年之后同样的问题再次出现。我写这篇不是来嘲笑谁的。代码永远会有 bug流程永远会有疏漏那个忘记删.map文件的工程师可能连续加班了一周。真正有价值的是把 source map 的生成、发布、泄露链路拆开看一遍然后给你一套可以直接复制到项目里的 npm 发布配置、关闭/校验命令以及 CI 中自动检测 source map 泄露的验证动作。适合谁看正在维护 TypeScript 库或 CLI 工具、需要往 npm 发版的团队尤其是用 Bun、esbuild、tsup、vite 这类默认带 source map 的构建工具的人。2. 先把 source map 的泄露链路讲清楚2.1 source map 是什么为什么它会泄露源码source map 本质是一个 JSON 文件里面记录了「压缩后代码的某个位置」对应「原始 TypeScript 文件的某一行某一列」的映射关系同时还带一个sourcesContent字段直接把原始源码内容嵌进去了。浏览器 DevTools 能还原源码靠的就是它。问题就出在sourcesContent。很多人以为 source map 只是「位置映射」删掉.map文件就没事了其实只要.map被发布出去攻击者用source-map这个 npm 包几行代码就能把原始.ts文件全部还原出来const { SourceMapConsumer } require(source-map); const fs require(fs); const raw JSON.parse(fs.readFileSync(./dist/index.js.map, utf8)); SourceMapConsumer.with(raw, null, consumer { consumer.sources.forEach(src { const content consumer.sourceContentFor(src); fs.writeFileSync(src.replace(/^.*\//, ), content); }); });这段代码跑完你的src/目录基本就回来了。2.2 泄露链路从构建到 npm 注册表把链路拆成四步每一步都是一个可能出事的环节环节常见默认行为风险构建工具Bun/esbuild/tsup 默认sourcemap: true或linked产物目录里出现.map打包配置files字段写[dist]或干脆不写.map被一起打进 tarball忽略规则没有.npmignore或.npmignore没写*.mapnpm 按默认规则打包发布校验npm publish前没有npm pack --dry-run检查泄露文件直接上传Claude Code 这次就是卡在第三步和第四步Bun 生成了.map.npmignore没排除发布前也没做 dry-run 校验。2.3 为什么「删了 .map 就安全」是错觉还有一种更隐蔽的情况构建时用了sourcemap: inlinesource map 被 base64 编码后塞进.js文件末尾的//# sourceMappingURLdata:application/json;base64,...注释里。这种情况下你删.map文件根本没用源码已经在.js里了。校验时必须同时检查这两种形态。3. TaoToken 前置把模型接入和发布校验串起来讲发布配置之前先说一个实际场景。很多团队在做 CI 自动检测时想让模型帮忙判断「这个 diff 里有没有引入 source map 相关风险」或者用 Claude Code 这类编码 Agent 直接改构建配置。这时候你需要一个稳定的模型接入点。TaoToken 是一个大模型 API 聚合平台兼容 OpenAI 和 Anthropic 的接口格式你可以用它来调用 Claude、GPT 等模型。对于本文场景它的用处有两个一是给 CI 里的自动化脚本提供模型调用能力二是给 Claude Code 这类编码工具提供后端。注册和拿 Key 的入口在这里官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content控制台创建 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基础地址是https://taotoken.net/api注意这个地址不带 UTM 参数直接用于代码里的baseURL。如果你只是想在 CI 里跑一个「检查 source map 泄露」的脚本其实不需要模型纯 Node 脚本就够了。但如果你想用 Claude Code 帮你自动修构建配置或者用模型做代码审查那就需要先配好 Key。下面给一个最小验证curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 回复 ok}] }返回里有choices[0].message.content就说明 Key 可用。这一步只是确认接入正常真正的重点在下面的发布配置。4. 可复制的 npm 发布配置与 source map 关闭命令4.1 构建阶段按环境关闭 source map不同构建工具的关闭方式不一样这里给几个常用的。tsuptsup.config.tsimport { defineConfig } from tsup; export default defineConfig({ entry: [src/index.ts], format: [esm, cjs], dts: true, sourcemap: process.env.NODE_ENV ! production, clean: true, });esbuild脚本里require(esbuild).build({ entryPoints: [src/index.ts], bundle: true, outfile: dist/index.js, sourcemap: process.env.NODE_ENV ! production, });Bunbun buildbun build ./src/index.ts --outdir ./dist --target node --sourcemapnone注意 Bun 的--sourcemap参数none才是完全关闭external会生成独立.map文件inline会塞进.js。vite库模式vite.config.tsexport default defineConfig({ build: { lib: { entry: src/index.ts, formats: [es, cjs] }, sourcemap: false, }, });4.2 打包阶段用 files 字段白名单 .npmignore 双保险package.json里的files字段是白名单机制写进去的才会被打包。推荐显式列出{ name: your-package, version: 1.0.0, main: ./dist/index.cjs, module: ./dist/index.js, types: ./dist/index.d.ts, files: [ dist/**/*.js, dist/**/*.cjs, dist/**/*.d.ts, README.md, LICENSE ] }注意这里没有dist/**/*.map也没有dist整个目录。如果你写files: [dist]那dist下所有文件包括.map都会被包含。再加一层.npmignore# 源码 src/ test/ tests/ *.ts !*.d.ts # source map *.map *.js.map *.css.map # 配置 tsconfig.json tsup.config.ts vite.config.ts .eslintrc* .prettierrc* # CI .github/ .gitlab-ci.yml注意files字段和.npmignore同时存在时files优先级更高。所以最稳的做法是files只写需要的.npmignore作为兜底。4.3 发布前npm pack --dry-run 校验这是最关键的一步也是 Claude Code 这次事故里缺失的一步。在npm publish之前先跑npm pack --dry-run输出会列出即将打包的所有文件。你要检查两件事有没有.map文件有没有src/目录。如果输出里出现dist/index.js.map或者src/xxx.ts立刻停下来修配置。更严格的写法直接让脚本在发现.map时退出码非零#!/usr/bin/env bash set -euo pipefail OUTPUT$(npm pack --dry-run 21) if echo $OUTPUT | grep -qE \.map$|\.map\s; then echo 检测到 source map 文件将被发布已阻止 echo $OUTPUT | grep -E \.map exit 1 fi if echo $OUTPUT | grep -qE ^\s*src/; then echo 检测到 src/ 目录将被发布已阻止 exit 1 fi echo 打包校验通过把这个脚本存成scripts/check-pack.sh在package.json里加{ scripts: { prepublishOnly: bash scripts/check-pack.sh npm run build } }prepublishOnly会在npm publish前自动执行校验不过就发不出去。5. 验证请求与成功结果CI 中自动检测 source map 泄露5.1 一个独立的 Node 检测脚本上面的 bash 脚本依赖npm pack在 CI 里够用。但如果你想在 PR 阶段就检测可以写一个不依赖打包的 Node 脚本直接扫描dist/目录// scripts/scan-sourcemap.mjs import { readdirSync, readFileSync, statSync } from node:fs; import { join, extname } from node:path; const DIST dist; const problems []; function walk(dir) { for (const name of readdirSync(dir)) { const full join(dir, name); const st statSync(full); if (st.isDirectory()) { walk(full); continue; } if (extname(name) .map) { problems.push(发现独立 source map 文件: ${full}); continue; } if (/\.(js|cjs|mjs)$/.test(name)) { const content readFileSync(full, utf8); if (content.includes(sourceMappingURLdata:)) { problems.push(发现 inline source map: ${full}); } if (/sourceMappingURL.*\.map/.test(content)) { problems.push(发现外部 source map 引用: ${full}); } } } } walk(DIST); if (problems.length) { console.error(source map 泄露检测未通过); problems.forEach(p console.error( - p)); process.exit(1); } console.log(source map 泄露检测通过);这个脚本同时覆盖了三种形态独立.map文件、inline base64、外部引用。5.2 接入 GitHub Actionsname: publish-check on: pull_request: push: branches: [main] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm ci - run: npm run build - name: 扫描 source map 泄露 run: node scripts/scan-sourcemap.mjs - name: 校验 npm 打包内容 run: bash scripts/check-pack.sh5.3 成功结果长什么样本地跑一遍npm run build node scripts/scan-sourcemap.mjs输出source map 泄露检测通过再跑打包校验bash scripts/check-pack.sh输出打包校验通过如果故意把sourcemap打开脚本会输出source map 泄露检测未通过 - 发现独立 source map 文件: dist/index.js.map - 发现外部 source map 引用: dist/index.js退出码为 1CI 直接红掉PR 合不进去。这就是你要的效果。6. 本篇常见错排查6.1 关了 sourcemap 但 dist 里还有 .map大概率是构建缓存没清。tsup 加clean: truevite 加emptyOutDir: true或者手动rm -rf dist再构建。另一个可能是你改了tsup.config.ts但实际跑的是package.json里的build脚本脚本里硬编码了--sourcemap配置文件的改动没生效。6.2 npm pack --dry-run 输出里没有 .map但发布后 npm 上还有检查是不是用了npm publish --access public之外的其他发布方式比如pnpm publish或yarn npm publish它们的打包规则和 npm 略有差异。另外确认你发布的是当前目录不是某个子目录。还有一种情况你本地删了.map但 CI 里构建时NODE_ENV没设成production导致 CI 构建产物带.map然后被发布。6.3 inline source map 检测漏了上面的脚本用content.includes(sourceMappingURLdata:)检测 inline。如果你的构建工具用了//# sourceMappingURLdata:application/json;charsetutf-8;base64,这种带 charset 的格式includes依然能匹配到sourceMappingURLdata:。但如果被压缩工具改成了//# sourceMappingURLdata:application/json;base64,且中间有换行就可能漏。更稳的写法是用正则if (/sourceMappingURLdata:application\/json/.test(content)) { problems.push(发现 inline source map: ${full}); }6.4 .npmignore 写了 *.map 但没生效.npmignore和.gitignore同时存在时npm 优先用.npmignore。如果你没有.npmignore但有.gitignorenpm 会拿.gitignore当忽略规则。所以如果你在.gitignore里写了dist/npm 打包时也会忽略dist/导致你的包发出去是空的。这种情况要么加.npmignore覆盖要么用files字段白名单。推荐后者更明确。6.5 用 Claude Code 改配置时改错了文件如果你用 Claude Code 这类编码 Agent 帮你改tsup.config.ts或package.json改完一定要跑一遍npm run build node scripts/scan-sourcemap.mjs验证。Agent 可能会把sourcemap: false改成sourcemap: true来「方便调试」或者把files字段改成[dist]。这类改动在 diff 里不显眼但后果严重。建议在 CI 里把scan-sourcemap.mjs设成必过项Agent 改错了也发不出去。7. 把校验动作固化下来比事后复盘有用Claude Code 这次泄露还原出来的代码里有一套六级权限验证系统还有一个专门在公共代码仓库操作时自动清除 AI 使用痕迹的 Undercover Mode。一个专门防止内部信息泄露的系统最终没能防住整个源码库的裸奔。这件事的讽刺之处在于安全设计做得再精细发布流程的最后一公里没守住前面全白搭。对你来说不需要搞六级权限也不需要 Undercover Mode。你只需要三件事构建时按环境关 source map打包时用files白名单加.npmignore双保险发布前跑npm pack --dry-run和scan-sourcemap.mjs。把这三个动作写进prepublishOnly和 CI下次谁改错了配置流水线会拦住他。如果你在 CI 里想加一层模型审查比如让模型读 diff 判断有没有引入 source map 风险可以用 TaoToken 的 API 接入模型对话测试模型可用性https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentCoding Plan长期编码/Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content但说实话source map 泄露这件事纯脚本检测比模型审查更可靠。模型可能漏判正则不会。先把scan-sourcemap.mjs跑起来再考虑要不要加模型层。最后提醒一句下次你发布代码时跑一遍npm pack --dry-run。至少别让全世界看到你的注释。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑