有时密钥或令牌必须出现在客户端打包产物中,因为浏览器要直接发起调用。混淆只改变了这个问题的一部分,而不是全部。
可读的密钥会被机器人发现,而不是人
以可读文本形式出现在已发布 JavaScript 中的密钥,任何人一次 grep 就能找到。爬取型密钥扫描器无需任何人手动打开你的网站就能发现它,而这种自动化发现正是密钥从「已发布」走向「被滥用」的常见路径。
不需要有人对你的产品感兴趣才会发生这件事。机器人会检查它能访问到的每一个公开打包产物,寻找形似凭证的字符串,而你的密钥和其他人的一起排在队列里。
编码密钥能买到什么,界限又在哪里
strings.encode 在任何正值的 complexity 目标下都会开启,因此源代码中的字符串字面量会在输出中被编码,而不是以文本形式发布。以形似凭证的字符串对打包产物做 grep 的扫描器,用这种方式找不到密钥。
这是有价值的,但也仅此而已。混淆隐藏的是一个值如何被使用,而不隐藏这个值本身:浏览器运行你代码时所需要的任何内容,都可以通过运行那段代码来获取。被封装起来的密钥并不是安全的密钥。
限制、代理、轮换,然后再混淆
按顺序做这四件事,并把混淆当作最后一步:
- 在提供方那里把密钥限制到它所提供的最窄范围、来源和配额。
- 把授权逻辑移到你自己的端点之后,让浏览器永远不持有真正的凭证。
- 轮换已经发布过的密钥。从它发布的那一刻起就假定它已经泄露。
- 混淆那些确实无法迁移到服务端的部分。
对于最后一步,默认构建已经会对它编码。要为 CI 设置构建门槛:
{
"strings": {
"leaks": {
"max": 0
}
}
}指令可以在 Pro 构建中把某一个值单独提升到高于程序其余部分的保护级别。
const apiKey = /* @afterpack preset=extreme */ "YOUR_SECRET";一旦出现可读字面量就让构建失败
--strings.leaks.max=0 会在可读字面量存活下来的那一刻让构建失败,并给出 DIAG_SURVIVING_READABLE_LITERAL 的名字。把它接入 CI,这样就不必靠肉眼检查了。要查看一个已部署的网站眼下暴露了什么,对它运行 npx afterpack@latest audit。
已发布的密钥依然是已发布的密钥
编码提高了通过阅读已发布文件来找到密钥的成本,一个通过了泄露门槛的构建是一个真实的结果。但这不代表这个值已经无法触及:它依然存在于输出中,只是被编码了,并会在你的代码使用它的那一刻被还原,因此任何在自己可控的浏览器中运行该页面的人都能把它读回来。轮换密钥并把授权逻辑迁移到你自己的服务器,才是真正消除暴露的步骤。