一个本不该离开你代码库的凭据、令牌或内部 URL,进入了你部署出去的 bundle。你需要知道此刻线上有什么,以及该怎么处理。
已发布的凭据是被扫描器发现的,而不是被人发现的
泄露的凭据从被提供服务的那一刻起就是一起正在发生的事故。发现路径是自动化的:密钥扫描器、bundle 爬虫或研究人员的脚本会在 JavaScript 文件中找到明文密钥,远早于有人坐下来阅读你的代码。无论你的监控面板显示什么,暴露的计时从部署时就已经开始了。
代价取决于凭据的权限范围。一个在共享账户上拥有宽泛权限的密钥,比一个范围很窄的密钥造成的事故更严重。两者只要能通过网络访问到,就都已经是事故。
查清你的线上站点暴露了什么
在做任何改动之前,先对已部署的站点运行 audit:
$ npx afterpack@latest audit https://yoursite.com它会针对访客浏览器此刻收到的 bundle,报告暴露的密钥、可访问的 source map,以及已发布的 JavaScript 受保护的程度。先看密钥和 source map 两部分。没有任何发现的报告,意味着这次扫描在这些 URL 上什么都没找到,而不能作为没有任何泄露的证据。
下一次构建会给这个值带来什么变化
默认预设会在构建时编码你的字符串字面量,因此内部端点、开关名称和凭据在你提供的文件中不再是明文。在新产物中 grep 这个密钥或它周围的文本,找不到它,而发现大多数泄露密钥的自动化爬虫也会一无所获。有些字面量在任何预设下都保持可读,Protection Map 会列出是哪些。
这为你争取到把轮换做完整的时间。它不能撤销这次泄露。旧的构建已经发出去了,而这个值仍然存在于新构建中。编码改变的是阅读者在文件里能找到什么,代码仍然持有这个值并使用它。
让 source map 和备份远离流水线
检查你的流水线中没有任何东西开启了那些会把原始源码交还出去的输出:
{
"sourceMap": { "enabled": false, "emitUrl": false, "sourcesContent": false },
"build": { "backup": false },
"protectionMap": { "enabled": true }
}source map 会把混淆后的文件映射回你可读的源码,而 sourceMap.sourcesContent 会把这份源码嵌入 map 之中。生产环境默认不生成 map,而一条为了调试而开启它们的流水线,通常就是 map 流落到服务器上的途径;当你需要生产环境的调用栈时,带 map 构建并把它保存在私有位置。
build.backup 生成的副本就是你原样的源码。CLI 把它保存在项目根目录下被 gitignore 忽略的 .afterpack/backups/ 中,位于你部署的内容之外。当框架插件把副本写在输出旁边、而你的部署会把整个目录原样发布时,把它设为 false。protectionMap.enabled 会把 Protection Map 写入构建机器上的 .afterpack/,CLI 拒绝把它写入被提供服务的路径。
确认问题已经消失
在轮换并重新部署之后再运行一次审计,确认之前的发现已经消失。加上 --diagnostics.format=json 可以在脚本中读取结果。如果构建报告了 DIAG_SURVIVING_READABLE_LITERAL,说明输出中仍有可读的字面量,而当剩余数量超过你设定的值时,strings.leaks.max 会让构建失败。这条诊断统计的是所有字面量,并不会告诉你哪一个是那个凭据。
混淆无法挽回的事
从现在起,编码会应用于每一次构建,因此明文值会从扫描器读取的产物中消失,而审计会告诉你线上站点今天暴露了什么。它做不到的,是触及已经发出去的那次构建:轮换凭据才是了结这起事故的那一步,而重新部署才能让旧的可读 bundle 不再被提供服务。
下一步
- 审计线上站点的 JavaScript:完整的检查清单,以及如何阅读报告。
- 最佳实践:按顺序排列的轮换步骤。
- 构建与 CI:让 source map 和备份远离每一个环境。