Skip to content

[REQ-128][CODE] fetchPackageAsset 补 timeout 与终态 URL 的 HTTPS/userinfo 复查 - #820

Merged
jinjunnn merged 2 commits into
alphafrom
feat/808-asset-fetch-hardening
Aug 3, 2026
Merged

[REQ-128][CODE] fetchPackageAsset 补 timeout 与终态 URL 的 HTTPS/userinfo 复查#820
jinjunnn merged 2 commits into
alphafrom
feat/808-asset-fetch-hardening

Conversation

@jinjunnn

@jinjunnn jinjunnn commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Fixes #808
Refs #699

大白话

宿主从 catalog 下载扩展包资产的那一跳,今天缺两个保护:

  1. 没有期限 —— 服务端只要保持连接、一个字节都不发,await fetch(...) 就永不落地。整个 admission 停在这里,没有超时、没有拒绝、没有错误。用户看到的是安装按钮永远转圈
  2. 不复查终态 URL —— response.url 是运行时给出的最终地址;payload 那条路已经在读它并校验 HTTPS/userinfo,资产这条路没有。

两条补齐后,与 payload 路径(package-installability.ts:37/539/543/546-550)逐条对称。这正是基线 §5 第 2 类「catalog → 宿主」那一行要补的东西。

范围

只有 fetchPackageAsset(packages/ui-mac/src/main/package-admission.ts)一个函数 + 一个模块常量。与载荷种类无关:凡是内容寻址资产都走这个函数。

票面原本含第 ① 半(assetRef 取值条件接受 script 资产形状),那半已由 #809 顺带做掉(package-admission.ts:529-538,随 alpha@5ef04016 落地),本 PR 一行未动。

判据(R1 审计 F10,假闸形态⑤/⑧)

admission 取资产时优先用注入的 helper:(deps.fetchAsset ?? fetchPackageAsset)(assetRef),而现有 package 测试大量注入它(package-update.test.ts 五处、package-admission.parity.test.ts 一处)⇒ 注入态下把这两道 guard 整段删掉,那些测试照样全绿

所以新增的 packages/ui-mac/src/main/package-asset-fetch.test.ts:

  • createPackageAdmissionCoordinator;
  • 不传 deps.fetchAsset,也不传 installability.fetchPayload(两条取回都落到生产下载器);
  • 只替换 globalThis.fetch

五条用例 = 三条 mutation + 两条控制:

用例 断的是 对应 mutation
正向控制 三个资产都经生产下载器取回、整包走到授权屏 挡住「什么都拒」的实现
mutation ① 服务端永不 EOF ⇒ 有界失败(signal.aborted === true + 拒绝理由恰是资产完整性那条 + 零事务调用) 删掉 setTimeout(...controller.abort())
mutation ②a 终态 URL 带 userinfo ⇒ 整包拒、不进授权屏 删掉终态 URL 复查
mutation ②b 终态 URL 非 HTTPS ⇒ 整包拒、不进授权屏 同上
过宽控制 终态 URL = 请求 URL 的干净 https ⇒ 仍须放行 挡住「有值就拒」/「终态≠请求就拒」—— 真 fetch 恒会填这个字段,写宽了会拒载全部真实安装

每条负例都同时断言「那个资产 URL 真的被请求过」+「它前面那个资产正常取回了」+「它后面那个资产没被请求」+「零事务调用」——只断 ok === false 会被「根本没下载就拒」的实现满足。

绕过实施记录(本机实测,原始输出)

mutation ① —— 把 setTimeout(() => controller.abort(), ASSET_TIMEOUT_MS) 改成 no-op、并从 fetch init 里摘掉 signal:

(fail) ... > mutation ①:资产服务端永不 EOF ⇒ admission 有界失败,不是挂死 [30001.20ms]
  ^ this test timed out after 30000ms.

 4 pass
 1 fail
Ran 5 tests across 1 file. [30.05s]

正是「挂死」而不是「拒绝」的指纹。用例显式声明 30s 超时(bun 默认 5s 会在生产 8s 期限之前杀掉用例 ⇒ 量到的是「跑得太久」不是「没有期限」,判据完全不同)。

mutation ② —— 删掉终态 URL 复查那 5 行:

300 |     expect(outcome).toMatchObject({ ok: false, reason: ASSET_REFUSAL })
error: expect(received).toMatchObject(expected)
(fail) ... > mutation ②a:终态 URL 带 userinfo ⇒ 整包拒绝,不进授权屏 [6.79ms]
316 |     expect(outcome).toMatchObject({ ok: false, reason: ASSET_REFUSAL })
error: expect(received).toMatchObject(expected)
(fail) ... > mutation ②b:终态 URL 非 HTTPS ⇒ 整包拒绝,不进授权屏 [3.90ms]

 3 pass
 2 fail
Ran 5 tests across 1 file. [8.06s]

收到的对象是 "stage": "authorize" 的完整授权预览 —— 即放行,不是别的什么理由的拒绝。

两次实验都在 git status 干净(改动已 commit)之后做,还原后 git diff 为空、git log 未动。

闸门登记

新文件登记进 scripts/gate-files.tsv(下界 5,不留余量)。理由:这 5 条跑在整包 bun test src 里,删掉整个文件是 3799→3794,远高于地板 3000 ⇒ 静默全绿,而两道 guard 一起消失。

本地门(真实输出)

bash scripts/alpha-check.sh —— 12/12 步全绿:

✓ 85 个闸门文件全部在位且真的跑过
    ✓ gate files
▶ [6/7] seed assets present (B7)
✓ seed/vendored resources present
▶ [7/7] docs gate ... ✓ no Markdown changed — docs gate is a no-op(与 CI 同)
✅ all local gates green — safe to push (alpha-ci will mirror this).

与 base(alpha@5ef04016)的差:

base 本 PR
ui-mac 3794 pass / 0 fail / 262 files 3799 / 0 / 263 +5 条 +1 文件(全部是新文件)
ext 133 / 0 / 10 133 / 0 / 10 0
contracts-consumer 54 / 0 / 5 54 / 0 / 5 0
闸门文件 84 85 +1(本次登记)

零回归。

诚实边界

终态 URL 复查是纵深保护,与 payload 侧同因同形 —— 不是「今天已知有一条路能绕过 redirect:"error"」。生产里 redirect:"error" 会在重定向时直接拒,所以真实终态 URL 恒等于请求 URL;这道复查管的是「我们这份 fetch 实现之外」的情形。代码注释里如实写成这样,没有夸大成一个可复现的攻击。

jinjunnn added 2 commits August 3, 2026 05:07
宿主取回资产这一跳此前只有 redirect:"error" 与字节双查,与 payload 那条路
(package-installability.ts:37/539/543/546-550)差两件:

① 无期限 —— 服务端保持连接不发 EOF,await fetch 就永不落地,整个 admission
   停在这里(没有超时、没有拒绝、没有错误),用户看到安装按钮永远转圈;
② 不复查终态 URL —— response.url 是运行时给出的终态地址,payload 路径已在读它。

两条补齐后逐条对称。抛出物被调用点吞成空字节 ⇒ 撞上 bytes/sha256 双查 ⇒ 整包具名拒绝。

判据(R1 审计 F10):真 createPackageAdmissionCoordinator、不传 deps.fetchAsset、
也不传 installability.fetchPayload,只替换 globalThis.fetch —— 注入态下把这两道
guard 整段删掉,现有 package 测试照样全绿。

Fixes #808
Refs #699
这 5 条跑在整包 `bun test src` 里,删掉整个文件是 3799→3794,远高于地板 3000
⇒ 静默全绿,而两道 guard 一起消失。整包地板管灾难,点名下界才管单文件消失。

Refs #808
@jinjunnn
jinjunnn merged commit 6ce6da4 into alpha Aug 3, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REQ-128][CODE] 宿主侧资产取回与完整性:script 资产形状 + fetchPackageAsset 补 timeout 与终态 URL 复查

1 participant