审计线上网站的 JavaScript

扫描一个线上 URL,检查公开的 source map、可读代码和泄露的凭据,并给出 100 分制的分数。与公开安全扫描器同款的扫描器,直接跑在你的终端里。

npx afterpack@latest audit <url> 会抓取网站发布的 JavaScript,并报告读者能从中提取出什么。它与公开的安全扫描器是同一个扫描器:公开的 source map、你自己代码的可读程度、泄露的凭据,以及不计分的第三方代码、动态代码注入点和检测到的技术栈。

audit 是只读的。它不会修改任何内容,不需要账户,只接触该 URL 公开提供的内容。

与混淆引擎相互独立的工具

audit 不会运行混淆引擎。它抓取网站已发布的资源,分析其中已经暴露的内容。构建参数对它都不适用。

运行

$ npx afterpack@latest audit https://yoursite.com

协议前缀是可选的:裸主机名会被规范化为 https://。扫描器会抓取页面并渲染它,以便包含客户端加载的代码包,然后分析找到的每一个 JavaScript 资源。

仅支持线上 URL

没有本地目录模式。不支持 npx afterpack@latest audit ./dist。参数必须是一个 URL。请改为审计一个预览部署。

它检查什么

检查项发现什么
公开的 source map你最大的几个脚本旁边的 .map 文件(通过响应头、注释或直接请求发现),以及它是否内嵌了原始源码。含源码的 map 会把分数上限压到 10,只含映射的 map 上限为 50。公开的 .git 目录等同于含源码的 map。
可读代码对你自己的每个脚本,衡量读者能以普通代码形式还原多少:英文字符串、有意义的名称和属性访问,基于词法单元测量。每个文件被评为原样、压缩或混淆。占 75 分,按字节加权。逐个 token 的版本参见Protection Map。
泄露的凭据私钥、云服务密钥、支付密钥、代码仓库和聊天令牌、数据库 URL 和 bearer token,打码后连同上下文显示。占 25 分。邮箱、IP 地址和可公开的浏览器密钥只列出,不计分。
第三方代码从网站提供的第三方 SDK,通过主机、路径或许可证注释识别。单独列出,不计入分数。
代码卫生eval()、Function 构造函数、document.write 和原始 HTML 注入点、第三方域名以及检测到的技术栈。只列出,不计分。

暴露的 source map

source map 会把已发布的代码包链接回你原始的、可读的源码。切勿把它发布到生产环境:攻击者下载它,就能像读你写的代码一样读出来。

audit 会抓出它。使用 AfterPack 的生产环境构建不会写出 .map,还会删除打包工具写出的 map(参见 sourceMap.enabled 和 sourceMap.emitUrl),但部署时从别处复制进来的 map 仍然会被发布出去。

解读结果

CLI 会在每个阶段完成时打印结果,critical 和 high 级别的发现会在出现时随即显示,最后以一个汇总框收尾:百分制得分、脚本数量、按严重程度分类的发现,以及检测到的技术栈。结尾会列出每一条严重发现,并附一个完整报告的网页链接。

从上到下阅读:

  1. 先看 source map。 公开的 map 会把你写的源码原样交给读者,不管构建做了什么。从生产环境中移除所有 .map 文件。
  2. 接着看凭据。 轮换任何泄露的凭据,然后停止发布它。
  3. 最后看可读代码。 如果你自己的文件被评为原样或压缩,那就没有实质性的保护。用 AfterPack 跑一次构建(快速上手大约五分钟)。

默认输出的是人类可读的文本;使用 --diagnostics.format=json 时,stdout 上只会输出一份 JSON 文档,其中包含发现、得分和报告链接,所有人类可读的内容都会转移到 stderr;再加上 --diagnostics.level=none 则会同时关闭进度阶段的输出。可分享的报告始终是这次运行结束时给出的那个网页链接。

未评级

页面没有网站自有代码,或扫描器无法读取这些代码时,扫描不会给分。报告显示 未评级 而不是一个数字,并说明原因:

  • 没有自有 JavaScript。 页面没有加载任何来自网站本身的脚本,因此没有可评级的内容。请扫描一个加载应用的页面,例如产品页面。
  • 被拦截。 网站用机器人验证或错误页面回应了扫描器,而不是页面本身。
  • 脚本未能读取。 页面加载了自有脚本,但扫描器无法读取其中的大部分。请重新扫描。

被拦截的扫描,或脚本未能读取的扫描,不会在 30 分钟的重复扫描窗口内被复用,因此新的扫描会立即运行。只有少数脚本无法读取时,扫描会按已读取的脚本评级,报告会注明漏掉了多少个。

退出码

退出码触发条件
0扫描已完成。即使发现了严重问题也是如此。
1扫描失败:URL 不可达、服务端错误、触发了每日速率限制,或者流在完成前中断。
64误用:没有给出 URL、给了不止一个 URL,或者 URL 格式不正确。

这些是 audit 自己的退出码。混淆运行使用的构建退出码是另外一套。

audit 不是 CI 门禁

因为完成的扫描无论发现了什么都会以 0 退出,npx afterpack@latest audit … && deploy 不会因为发现问题而失败。没有严重程度阈值参数:如果你想据此做门禁判断,请自己从 --diagnostics.format=json 中读取发现结果。把 audit 当作一个由你手动阅读的发布前审查步骤来用。会让构建失败的环节是混淆运行本身。

未认证的扫描每个 IP 每 24 小时限 3 次,同一个网站每小时最多扫描 10 次。达到限制时会打印还需等待多久,并以 1 退出。30 分钟内重复扫描同一个 URL 会返回现有报告,不计入次数。

AfterPack 徽章

把徽章放在网站页脚或 README 中。当你的主机的最新扫描发现网站自身代码是 AfterPack 输出、没有公开的 source map 或凭据、且评级为 A 时,它显示 Protected by AfterPack;否则显示最新分数;你的主机还没有任何扫描为网站评级时显示 not graded。从未扫描过的主机显示 Scan with AfterPack。徽章链接到最新报告,并在新扫描后一小时内更新。

AfterPack badge

页脚:

<a href="https://www.afterpack.dev/security-scanner/site/yoursite.com?ref=badge" rel="noopener" title="AfterPack security scan">
  <img src="https://www.afterpack.dev/badge/yoursite.com.svg?style=compact&theme=dark" alt="AfterPack badge" width="200" height="32" loading="lazy">
</a>

README:

[![AfterPack badge](https://www.afterpack.dev/badge/yoursite.com.svg)](https://www.afterpack.dev/security-scanner/site/yoursite.com?ref=badge)

样式:页脚用 compact(200×32)和 wide(236×48),README 用 flat(20 px)。主题:dark 和 light。安全扫描器的每个结果页都有该主机的这些代码片段。

下一步

  • 构建与 CI:把混淆嵌入你的发布流水线,并固定一个 seed 以获得可复现的输出。
  • 最佳实践:该标记什么、该保留什么不动,以及 source map 的规则。
  • 安全扫描器:在浏览器里做同样的检查,无需安装,附完整的可分享报告。
  • Protection Map:一旦你开始混淆,可以查看你自己构建的逐 token 视图。
  • AfterPack 不能替代什么:泄露的密钥需要轮换;事后混淆它没有用。