130 lines
6.5 KiB
Markdown
130 lines
6.5 KiB
Markdown
# XHR/fetch 请求语义与生命周期审计
|
||
|
||
本文件用于解决“XHR/fetch 已桥接同一 TLS Session,但请求头、body、请求来源、失败语义、responseURL、事件顺序或 reload 生命周期仍错误”的问题。Session Bridge 只证明请求由哪个客户端发送;本审计证明发送的请求和浏览器行为是否正确。
|
||
|
||
## 核心原则
|
||
|
||
- 真实请求前必须先执行 `no-send` 审计:Node bridge 记录完整待发送请求,但不得访问目标网络。
|
||
- 浏览器成功样本与 Node no-send 结果必须生成可比较的 network transcript。
|
||
- document navigation、reload、image、script、XHR、fetch、sendBeacon 必须有明确 actor,禁止根据 URL 猜请求类型。
|
||
- 请求和响应的 `matched` 必须由 `check_xhr_fetch_semantics.js` 计算,不能手工填写。
|
||
- Header 名称、顺序、重复字段和 body 原始字节属于请求语义,不得只比较对象键值。
|
||
- 缺失字段不得在规范化时用 `""`、`0`、`main` 或数组默认值掩盖。浏览器与 Node 同时缺失同一字段也必须失败。
|
||
- transcript 中敏感 Header 可以只记录 `{ "redacted": true, "length": N, "sha256": "..." }`;不允许只保留 Header 名称或空字符串。
|
||
|
||
## 强制产物
|
||
|
||
```text
|
||
case/fixtures/browser-network-transcript.ndjson
|
||
case/tmp/node-network-transcript.ndjson
|
||
case/tmp/xhr-fetch-semantics-audit.json
|
||
case/notes/xhr-fetch-semantics-diff.md
|
||
```
|
||
|
||
两份 transcript 都必须记录 `baselineId`。Node transcript 还必须记录:
|
||
|
||
```json
|
||
{
|
||
"type": "meta",
|
||
"schemaVersion": "network-transcript/v3",
|
||
"generatedBy": "runtime-network-recorder/v3",
|
||
"baselineId": "fp-001",
|
||
"mode": "no-send",
|
||
"runtimeSourceHash": "...",
|
||
"networkAttempts": 0
|
||
}
|
||
```
|
||
|
||
浏览器 meta 必须使用 `mode: "browser-baseline"`,并记录 `baselineId` 与可信 `generatedBy/capturedBy`。Node meta 必须使用 `mode: "no-send"`,记录当前源码的完整 SHA-256 `runtimeSourceHash`、可信 `generatedBy` 和显式 `networkAttempts: 0`。
|
||
|
||
## 每个事件共同字段
|
||
|
||
- `kind`、`pairKey`、`actor`。
|
||
- 显式 `realmId`、`navigationEpoch`、`sequence`,不能依赖默认值。
|
||
- `sessionId`;同一 transcript 只允许一个 Session。
|
||
|
||
## 每个请求必须记录
|
||
|
||
- method、完整 URL、query。
|
||
- 有序 Header 数组,保留重复字段。
|
||
- credentials、referrer、origin、Cookie 处理策略。
|
||
- body 类型、编码、原始 byte length、SHA256。
|
||
- Content-Type 和 multipart boundary。
|
||
- redirect、cache、timeout、abort 信息。
|
||
|
||
`POST`、`PUT`、`PATCH` 或任何实际携带 body 的请求,必须存在 `contentType`、原始 `bodyLength` 和完整 `bodySha256`。没有请求体时也不得伪造调试字符串的 hash。
|
||
|
||
Header 推荐结构:
|
||
|
||
```json
|
||
[
|
||
["content-type", "text/plain;charset=UTF-8"],
|
||
["cookie", {"redacted": true, "length": 128, "sha256": "..."}]
|
||
]
|
||
```
|
||
|
||
Header 数组中的每一项都必须有非空名称和真实值或脱敏摘要。比较时使用名称、顺序、重复次数、byte length 与 SHA-256;不能只比较名称集合。
|
||
|
||
浏览器与 Node 的原始 request id 可以不同,但必须在规范化阶段生成相同 `pairKey`。推荐使用“writer phase + actor + realm role + navigation epoch + 该 actor 第 N 次请求”,不得只用 URL 配对,因为同一 URL 可能同时用于 document navigation、reload 和 XHR。
|
||
|
||
## 每个响应必须记录
|
||
|
||
- status、statusText、responseURL。
|
||
- 响应头有序列表和脚本可见头列表。
|
||
- body byte length、SHA256、responseType。
|
||
- Set-Cookie 是否写入同一 session Cookie jar。
|
||
- readyState、readystatechange、load、error、abort、timeout、loadend 顺序。
|
||
- Promise resolve/reject 与 microtask/task 顺序。
|
||
- PerformanceResourceTiming 可见时机。
|
||
|
||
即使响应体为空,也必须记录 `bodyLength: 0` 和空字节 SHA-256。`status > 0` 时必须记录最终 `responseURL`;失败、abort、CORS 或网络错误的 `status=0` 必须显式记录空 `responseURL`。
|
||
|
||
## 硬性浏览器语义
|
||
|
||
- 网络错误、CORS 拒绝、abort 等 `status=0` 场景,`responseURL` 必须为空,除非同 baseline 浏览器证据明确相反。
|
||
- completion 层不得用 request URL 为失败 XHR 回填 `responseURL`。
|
||
- document navigation/reload 不得进入 XHR completion。
|
||
- 首次 navigation 与 reload 必须创建各自正确的 document/realm 生命周期。
|
||
- reload 或 navigation commit 后,旧 realm 的 pending timer、XHR、fetch、MessagePort task 和 observer callback 必须取消或丢弃。
|
||
- 真实浏览器没有发出的 XHR/fetch,Node 不得多发;多余事件直接阻断。
|
||
- `getResponseHeader("set-cookie")`、Response Headers 的 Set-Cookie 可见性必须以目标浏览器 baseline 为准。
|
||
- browser/Node 必须逐事件保持 request、response、reload/navigation lifecycle 的全局顺序;只按 `pairKey` 分组后忽略相对顺序不能通过。
|
||
- Node transcript 必须来自真正执行当前 XHR/fetch 实现的 recorder;手写与浏览器相同的 NDJSON、复制浏览器事件或空值占位都不算证据。
|
||
|
||
## body 类型矩阵
|
||
|
||
目标路径涉及时至少覆盖:
|
||
|
||
- `null` / `undefined`。
|
||
- string。
|
||
- `URLSearchParams`。
|
||
- `FormData` 与 multipart boundary。
|
||
- Blob。
|
||
- ArrayBuffer / TypedArray。
|
||
- Document。
|
||
- JSON string。
|
||
|
||
必须比较发送前 body byte length 和 SHA256,不能只比较序列化后的调试文本。
|
||
|
||
## Header 矩阵
|
||
|
||
必须检查:
|
||
|
||
- `setRequestHeader()` 的 trim、合并和重复行为。
|
||
- Header 名称大小写与发送顺序。
|
||
- Header 实际值或脱敏后的 byte length / SHA-256;只有名称没有值直接失败。
|
||
- 自动 Content-Type。
|
||
- Cookie、Origin、Referer、Accept、Accept-Language、Client Hints 等由哪一层生成。
|
||
- 禁止脚本设置的 Header 是否被正确过滤。
|
||
- curl/HAR 与当前浏览器 baseline 冲突时,以当前 baseline 为准。
|
||
|
||
## 检查命令
|
||
|
||
```bash
|
||
node scripts/check_xhr_fetch_semantics.js --case-dir case --require --require-no-send --out case/tmp/xhr-fetch-semantics-audit.json --markdown
|
||
node scripts/check_xhr_fetch_session_bridge.js --case-dir case --require-live --tls-client curl_cffi --markdown
|
||
node scripts/check_environment_closure.js --case-dir case --before-real-request --require-live --tls-client curl_cffi --markdown
|
||
```
|
||
|
||
只有 no-send 语义审计和 session bridge 运行时审计同时通过,才允许少量授权真实请求。不得等用户提出“比较请求头和请求体”后才执行本流程。
|