fix(connection): keep LAN serving working under the /api browser-trust fence
Markerless requests pass on any Host (a non-browser sender is the principal and forges headers anyway); browser Host matching gains port-less entries and WHATWG normalization; dsh derives LAN IP-literal authorities for an all-interfaces bind and web grows --trusted-host for named ones.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md
|
||||
2026-07-28-api-browser-trust-boundary.md: c620f1a65e3890bbd2580415e55b25436fefe36e
|
||||
2026-07-28-api-browser-trust-boundary.zh.md: 0452eff1017b2f70a00e67c5cfce8dba3a840539
|
||||
2026-07-28-api-browser-trust-boundary.md: 45a332fcfe59fb930a85cfc595dd02c5fe12a5d7
|
||||
2026-07-28-api-browser-trust-boundary.zh.md: 731d6c81f71a2f50b716e52e278f2c53ad62a04b
|
||||
|
||||
@@ -13,7 +13,7 @@ The web GUI host serves `/api` over plain HTTP (default `127.0.0.1:3080`, `--hos
|
||||
Enforce browser trust once, at the carrier, for the entire `/api` prefix — two halves in two stacked PRs:
|
||||
|
||||
- **Media-type fence (dsh-host-apiproxy)**: every `/api` POST must declare `application/json`, else 415 before parsing. Cross-site "simple" requests thereby stop existing: any cross-site attempt is forced into a CORS preflight this server never answers.
|
||||
- **Authority fence (dsh-client-connection, `src/api-request-trust.ts`)**: `Host` must be loopback or an exact `host[:port]` from the plugin's `trustedHosts` config (rebinding defense); an attached `Origin` must equal that authority; `sec-fetch-site: cross-site` is refused outright. Requests without browser markers pass — a non-browser client is the principal itself, not a deputy. `host.pickDirectory` loses its bespoke guard and rides the same fence.
|
||||
- **Authority fence (dsh-client-connection, `src/api-request-trust.ts`)**: requests without browser markers (no `Origin`, no `sec-fetch-site`) pass on any Host — a non-browser client is the principal itself, not a deputy, and forges every header anyway, so fencing it buys nothing and breaks non-browser LAN automation. For browser requests, `Host` must be loopback or match a `trustedHosts` entry (exact on `host:port`, any port on port-less entries, WHATWG-normalized; rebinding defense); an attached `Origin` must equal that authority; `sec-fetch-site: cross-site` is refused outright. `host.pickDirectory` loses its bespoke guard and rides the same fence.
|
||||
|
||||
Two boundaries stay deliberately out of scope: reachability is the webserver binding's policy (`host: 127.0.0.1 | 0.0.0.0`), and authentication for genuinely remote deployments is deferred work recorded in the connection README — the fence is a confused-deputy defense, not an auth layer. The old guard's loopback-socket check was dropped rather than generalized: with binding expressing reachability and `trustedHosts` naming remote authorities, the socket address adds nothing a header fence does not already cover.
|
||||
|
||||
@@ -26,6 +26,6 @@ Two boundaries stay deliberately out of scope: reachability is the webserver bin
|
||||
## Consequences
|
||||
|
||||
- Any future `/api` method is covered by construction; there is no per-route trust decision left to forget.
|
||||
- Non-loopback deployments must declare their serving authorities in `trustedHosts` or browsers are refused; plain curl-shape automation is unaffected either way.
|
||||
- Non-loopback deployments must have their serving authorities trusted or browsers are refused. The dsh CLI keeps its advertised `--host 0.0.0.0` LAN URL working by deriving the machine's LAN IP literals into the connection row (port-less entries — an IP-literal Host cannot be a rebound name, and the bound port may be OS-assigned) and offers `dsh web --trusted-host` for named authorities; compositions the CLI does not boot declare `trustedHosts` themselves. Plain curl-shape automation is unaffected everywhere.
|
||||
- Clients must label POST bodies `application/json` (ours always did; raw-fetch tests gained the header).
|
||||
- The trusted-network assumption of an unauthenticated `0.0.0.0` deployment is now documented instead of implicit.
|
||||
|
||||
@@ -13,7 +13,7 @@ Web GUI 宿主以纯 HTTP 提供 `/api`(默认 `127.0.0.1:3080`,支持 `--ho
|
||||
在载体层对整个 `/api` 前缀一次性执行浏览器信任检查——两半各占一个栈式 PR:
|
||||
|
||||
- **媒体类型栅栏(dsh-host-apiproxy)**:每个 `/api` POST 必须声明 `application/json`,否则在解析前以 415 拒绝。跨站"简单请求"由此不复存在:任何跨站尝试都被逼进一次本服务器从不应答的 CORS 预检。
|
||||
- **权威栅栏(dsh-client-connection,`src/api-request-trust.ts`)**:`Host` 必须是回环地址,或与插件 `trustedHosts` 配置中的某个 `host[:port]` 精确匹配(rebinding 防御);若带 `Origin` 则必须与该权威完全一致;`sec-fetch-site: cross-site` 一律拒绝。不带浏览器标头的请求放行——非浏览器客户端是委托人本人,不是代理人。`host.pickDirectory` 失去专属守卫,与其他请求同栅而行。
|
||||
- **权威栅栏(dsh-client-connection,`src/api-request-trust.ts`)**:不带浏览器标记的请求(无 `Origin`、无 `sec-fetch-site`)在任何 Host 上都放行——非浏览器客户端是委托人本人,不是代理人,且本就可以伪造任何请求头,对它设栅一无所获,反而会打断非浏览器的 LAN 自动化。对浏览器请求,`Host` 必须是回环地址,或与某个 `trustedHosts` 条目匹配(带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,均经 WHATWG 归一化;rebinding 防御);若带 `Origin` 则必须与该权威完全一致;`sec-fetch-site: cross-site` 一律拒绝。`host.pickDirectory` 失去专属守卫,与其他请求同栅而行。
|
||||
|
||||
两条边界刻意留在范围之外:可达性归 webserver 绑定配置(`host: 127.0.0.1 | 0.0.0.0`)管辖;真正远程部署的认证是延期工作,记录在 connection README——这道栅栏是混淆代理人防御,不是认证层。旧守卫的回环 socket 检查被放弃而非泛化:绑定表达可达性、`trustedHosts` 点名远程权威之后,socket 地址提供不了头部栅栏覆盖不到的任何东西。
|
||||
|
||||
@@ -26,6 +26,6 @@ Web GUI 宿主以纯 HTTP 提供 `/api`(默认 `127.0.0.1:3080`,支持 `--ho
|
||||
## 后果
|
||||
|
||||
- 未来任何 `/api` 方法天然在覆盖范围内;不存在会被遗忘的按路由信任决定。
|
||||
- 非回环部署必须在 `trustedHosts` 中声明服务权威,否则浏览器会被拒绝;curl 形态的自动化不受影响。
|
||||
- 非回环部署的服务权威必须获得信任,否则浏览器会被拒绝。dsh CLI 通过把本机 LAN IP 字面量推导进 connection 行(不带端口的条目——IP 字面量 Host 不可能是被重绑的域名,且绑定端口可能由操作系统分配)来保住它广告出的 `--host 0.0.0.0` LAN URL,并提供 `dsh web --trusted-host` 声明具名权威;CLI 不参与引导的组合自行声明 `trustedHosts`。curl 形态的自动化在任何地方都不受影响。
|
||||
- 客户端必须给 POST 体标注 `application/json`(我们自己的客户端一向如此;裸 fetch 测试补上了该头)。
|
||||
- 无认证 `0.0.0.0` 部署的"信任网络"假设从隐含变为成文。
|
||||
|
||||
Reference in New Issue
Block a user