Relintio/relintio-inspector
GitHub: Relintio/relintio-inspector
Relintio Inspector 是一款 Chrome 扩展,用于检测页面上是否暴露了 Relintio license key 以及 agent 是否正常工作。
Stars: 0 | Forks: 0
该页面是否受 Relintio 保护,以及是否有 license key 发送到了浏览器。第二个问题正是该扩展存在的意义。license key (`UP_LIVE_…`) 是用于签署 challenge passport 和出站请求签名的 HMAC 密钥:如果它被遗留在前端 bundle 中,就等于让任何打开 devtools 的人都能绕过 WAF,而且它不会主动暴露自己。这既不是一个 SDK,也不是一个 npm 包 —— `package.json` 的 `private` 属性为真,且这里没有任何需要安装到应用中的东西。它只是一个工具栏按钮,在你主动要求时,对单个页面进行一次读取。其入口点是 `src/popup.js`,它会将 `collectFromPage` 注入到当前活跃的标签页中,并将返回的结果渲染出来。
## 安装说明
目前尚未上架 Chrome Web Store —— 相关列表正在审核中,此目录下的 `STORE_LISTING.md` 是提交的草稿。在此之前,请以未打包方式加载它:
1. 打开 `chrome://extensions`
2. 开启 **开发者模式**
3. 点击 **加载已解压的扩展程序**,并选择此目录
不需要构建步骤。当前目录结构即为完整的扩展:包含 `manifest.json`、`src/` 中的三个文件以及四个图标。
## 它的检测目标
在任何 `http` 或 `https` 页面点击工具栏图标。在点击之前不会运行任何操作。
**凭证(Credentials)。** 包含两种模式,检测范围涵盖页面标记、每个内联脚本以及它能获取到的每个外部 bundle。
| 模式 | 匹配对象 | 严重程度 |
| --- | --- | --- |
| `UP_LIVE_` 或 `UP_TEST_` + 8 位或更多 `[A-Za-z0-9_-]` | license key | **严重** |
| `pk_live_` 或 `pk_test_` + 16 位或更多字母数字 | publishable key | 信息 |
对于 publishable key,将其报告为符合预期,而不是作为发现漏洞。它在设计上就是公开的,仅限定于请求判定结果,且本就属于前端代码的一部分 —— 如果标记它,只会导致人们习惯性忽略面板,从而让真正的密钥蒙混过关。测试用的 license key 与正式版一样同属严重级别:它仍然是一个密钥,且在生产环境中出现测试密钥通常意味着正式密钥存在于某个行为类似的代码分支中。
长度下限的限制是为了将普通文本排除在结果之外。例如 `Set your UP_LIVE_ key in the environment` 这样的描述不会匹配,并且有专门的测试用例对此进行了验证。
**检测 agent 是否存在**,这依赖于多个独立信号,因为单独依靠任何一个信号都会在某些情况下失效 —— bundler 可能会重命名包,单页应用在访客进行操作前可能不会发起判定请求,CDN 可能会剥离 headers。
| 信号 | 证据 | 来源 |
| --- | --- | --- |
| Package | 源码中的 `@relintio/`,或包含 `relintio` 的脚本 URL | Scripts 与标记 |
| Traffic | 对 `/agent/decision`、`/agent/verify` 或 `/agent/log` 的调用 | Resource Timing 缓冲区 |
| Passport | `relintio_passport` cookie | `document.cookie` |
| Headers | 任何 `x-relintio*` 响应头 | 目前不收集 —— 见下文 |
浏览器对 `/agent/verify` 的调用会被直接升级为严重级别。该 endpoint 会返回完整的策略,且只应由持有 license key 的 agent 调用,因此如果浏览器能访问它,就意味着浏览器中已经存在密钥 —— 即便扫描无法在源码中找到它(它可能是在 runtime 组装的、从 base64 解码出来的,或者是从其他地方获取的)。
## 展示内容
发现的凭证会被进行脱敏处理。只显示十二个字符及计数 —— 例如 `UP_LIVE_a1b2…24 more` —— 这足以让你在自己的源码中进行 grep 搜索,但又不足以被他人直接利用。该弹窗的截图绝不能成为密钥被泄露的第二场所,因此测试套件严格断言尾部内容绝不会出现在发现的任何位置,而不仅仅是在 helper 函数中。
同一个密钥在多处被发现只算作一次泄露。页面标记及其包含的内联脚本是分开收集的,因此内联脚本中的密钥确实会被检测到两次;摘要统计的是去重后的密钥数量,因为把一个失误说成“有 3 个可读的 license key”只会导致用户卸载这个扩展。
面板绝不会断言某个网站未受保护。服务端 agent 会在任何内容到达浏览器之前进行拦截,且不会在这里留下任何可检测的痕迹,因此当所有信号均缺失时,报告只会显示为*无可见内容*。同时,它也会明确告知无法读取的内容:缺乏 CORS 配置的 bundle 会被列入 **What this scan could not see** 部分中,而不是被草率地计为安全。
无需任何配置。没有选项页面,没有存储,没有账户,也不会在弹窗关闭后保留任何状态。
## 权限
仅需要两项权限,且完全不需要主机权限。如果一个安全厂商的扩展需要读取你访问的每个网站,这种代价通常比它解决的问题还要大。
| 权限 | 需求原因 |
| --- | --- |
| `activeTab` | 仅在你点击工具栏图标时授予对当前标签页的访问权限,且仅对该次点击有效。正是它允许扩展读取你正在检查的页面,并且该权限会自动过期 —— 你无法以这种方式访问未打开过弹窗的标签页。 |
| `scripting` | `chrome.scripting.executeScript` 所必需,用于在该标签页中执行一次性数据收集函数。正是它避免了在每个页面注入 content script。 |
未声明任何其他权限。没有 `host_permissions`,没有 `background` service worker,没有 `storage`,没有 `webRequest`,也没有 `cookies`。manifest 为扩展页面设置了 `script-src 'self'; object-src 'none'`,且所有代码都已打包在程序中 —— 不会在 runtime 获取或执行任何外部代码。
该扩展自身不会发起任何网络请求。唯一的 fetch 请求是为了获取页面自身的脚本 URL,且是在该页面内部发起,并带有 `credentials: 'omit'` 参数。你查看的任何内容都不会被发送到任何地方,因为根本不存在可以发送的目标地址。
`src/popup.js` 使用 `createElement` 和文本节点构建输出,并且绝不会将页面来源的内容赋值给 `innerHTML`。它显示的所有内容 —— 脚本 URL、header 值、标签 —— 都受被检查页面的控制,而弹窗是在扩展自身的 origin 和权限下运行的。
## 扫描局限性(无法看到的内容)
**Headers 信号目前无法触发。** `detectAgent` 接受一个 `headers` 对象并查找 `x-relintio*`,且有针对此的测试,但 `collectFromPage` 从不收集响应头,`popup.js` 也从不传递任何响应头。商店列表中描述了四种信号;目前只有三种能将结果反馈到面板。在提交之前,要么把 headers 的逻辑贯穿到代码中,要么从 `STORE_LISTING.md` 中删掉相关描述。
**Passport 信号的触发本身就是一个漏洞发现,但并未作为漏洞上报。** Cookie 来源于 `document.cookie`,这会排除 `HttpOnly` cookie,而每个 Relintio agent 都会将 `relintio_passport` 设置为 `HttpOnly`。因此,在正确的安装配置下,该信号永远不会出现 —— 而在它确实出现的错误配置中,面板却将其报告为受保护的有力证据,而不是将其视为一种配置错误。
**超过 2 MB 的源文件会被静默截断。** `MAX_SOURCE_BYTES` 的限制是每个源文件 2,000,000 字节,超出部分会被直接丢弃,且输出中不会有任何提示 —— 位于大型 bundle 偏移量之后的密钥会被漏掉,而扫描依然会报告状态安全。相比之下,40 个源文件的上限处理得就很得当:达到限制时程序会停止扫描,并提示已停止。
**该上限会统计所有内容。** 页面标记和每个内联脚本都会占用名额,但只有外部脚本分支会检查限制,因此包含大量内联脚本的页面可能会在获取第一个 bundle 之前就耗尽预算。
**读取的是缓存中的 bundle。** fetch 使用了 `cache: 'force-cache'`,因此扫描的内容是浏览器已持有的副本。如果某次部署移除了密钥,但浏览器尚未更新缓存,该密钥仍会被找到;反之,如果部署新增了密钥,则可能无法被检测到。
**无法读取跨域 bundle。** 如果脚本没有配置 CORS,页面就无法读取它,该扩展同样也无法读取。此类情况会被明确报告,而不是被计为安全。
**流量证据来自 Resource Timing 缓冲区**,因此它涵盖的是文档自加载以来已经获取的内容,而不是接下来的操作。在你打开弹窗之后发起的判定请求不会被记录在内;请关闭弹窗并重新加载页面后再进行查看。
**某些页面对所有扩展都是禁止访问的** —— 例如 `chrome://` 页面、Web Store 以及其他少数页面。遇到这种情况,弹窗会予以提示,而不是显示空白结果。
## 开发说明
```
npm test
```
包含 24 个测试,无外部依赖,使用 `node --test` 运行。`src/scan.js` 包含了模式匹配、脱敏处理、检测和汇总逻辑,并且是纯函数式的 —— 不涉及 `chrome.*`,也不涉及 DOM —— 因此所有逻辑都可以在 Node 中进行测试。这也是整个测试套件的核心目标,因为这个扩展中唯一可能对他人造成损害的途径就是:漏报已公开的密钥、打印出密钥,或者因为对 publishable key 的误报而导致无人再关注面板的警告。
编辑代码后,请从 `chrome://extensions` 重新加载扩展。没有文件监听器,也无需编译。
## 链接
- [文档](https://relintio.com/docs)
- [API 参考](https://relintio.com/docs/api-reference)
- [许可证](https://relintio.com/licenses)
- [`STORE_LISTING.md`](./STORE_LISTING.md) —— 待提交的 Chrome Web Store 审核草稿,包含上文中的权限说明
安全报告请发送至 **support@relintio.com**,请勿提交至公开的 issue。
## 开源协议
MIT 协议。详见 [`LICENSE`](./LICENSE)。
标签:Chrome插件, StruQ, WAF, Web安全, 前端安全, 数据可视化, 浏览器扩展, 自定义脚本, 蓝队分析