6.5 KiB
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 名称或空字符串。
强制产物
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 还必须记录:
{
"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 推荐结构:
[
["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 为准。
检查命令
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 运行时审计同时通过,才允许少量授权真实请求。不得等用户提出“比较请求头和请求体”后才执行本流程。