Răsfoiți Sursa

fix(web): address wildcard host review

Tianyi Cui 1 lună în urmă
părinte
comite
0a42836fbb

+ 2 - 2
apps/cli/tests/args.spec.ts

@@ -36,8 +36,8 @@ describe('parseDshArgs', () => {
       .toEqual({ mode: 'profile', profile: 'tui', patches: [], args: ['--resume', 'abc'] })
     expect(parse(['--profile', 'web', '-h']))
       .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['-h'] })
-    expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev']))
-      .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '0.0.0.0', '--port', '8080', '--dev'] })
+    expect(parse(['web', '--host', '127.0.0.1', '--port', '8080', '--dev']))
+      .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '127.0.0.1', '--port', '8080', '--dev'] })
     expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
       .toEqual({ mode: 'profile', profile: 'headless', patches: [], args: ['run', 'the', 'tests'] })
     // Launcher flags placed after that boundary belong to the app too.

+ 9 - 0
apps/cli/tests/built-bin.e2e.ts

@@ -339,6 +339,15 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
       expect(web.stdout).toContain('--port <port>')
       expect(web.stdout).not.toContain('dsh web: http://')
 
+      const wildcardHost = await runBuiltBin(['web', '--host', '0.0.0.0'], {
+        DSH_HOME: home,
+        DSH_TELEMETRY_DISABLED: '1',
+      })
+      expect(wildcardHost.code).toBe(1)
+      expect(wildcardHost.stdout).toBe('')
+      expect(wildcardHost.stderr).toContain('--host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead')
+      expect(wildcardHost.stderr).not.toContain('dsh web: http://')
+
       const headlessHelp = await runBuiltBin(['--profile', 'headless', '--help'], {
         DSH_HOME: home,
         DSH_TELEMETRY_DISABLED: '1',

+ 3 - 3
packages/bundle/web-app/src/startup.ts

@@ -57,9 +57,9 @@ Examples:
 
 /**
  * Parse and provide the Web invocation as an ordinary Cordis service. The
- * command's action publishes the flags this invocation named; an unsupported
- * `--host` or non-numeric `--port` is a usage error, so on rejection (and on
- * `--help`) nothing is provided.
+ * command's action publishes the flags this invocation named; `--host 0.0.0.0`
+ * or a non-numeric `--port` is a usage error, so on rejection (and on `--help`)
+ * nothing is provided.
  * @param ctx - plugin context carrying the command line.
  */
 export function apply(ctx: Context): void {

+ 2 - 2
packages/client/connection/README.i18n.yaml

@@ -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 packages/client/connection/README.md
-README.md: 3315552dd0400697d5a96639598c5f5bb2bb4f5b
-README.zh.md: 0dc251f1e5410bd0d74ef450c6d8df3dc9cdab81
+README.md: f0a707cc5f3a962c7852f7323c727d0a39a57b10
+README.zh.md: b529eebf9af93e36b6b92c19964678b7f5a04ea1

Fișier diff suprimat deoarece este prea mare
+ 0 - 1
packages/client/connection/README.md


+ 1 - 1
packages/client/connection/README.zh.md

@@ -6,7 +6,7 @@
 
 ## /api 浏览器信任栅栏
 
-node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-request-trust.ts`)。每个请求——无论是否带浏览器标记——`Host` 都必须是回环地址权威,或与某个 `trustedHosts` 条目匹配:带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 `Origin` 并通过同一道比较。非浏览器客户端经由回环地址、CLI(命令行界面)推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,如附带 `Origin`,则它必须与 Host 权威完全一致;显式的 `sec-fetch-site: cross-site` 标记一律拒绝。不是纯的、规范形 `host[:port]` 权威的 `trustedHosts` 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载明确报错:否则解析会悄悄授权 `harness.internal/path` 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何事件流前拒绝握手。因此非回环(`--host 0.0.0.0`)部署需要让自己的服务权威被信任:dsh CLI 会自行推导本机的 LAN IP 字面量,其 `--trusted-host` flag 用于声明具名权威,所以 cordis.yml 中的 `trustedHosts` 面向 CLI 不参与引导的组合。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:[api 浏览器信任边界 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md)。
+node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-request-trust.ts`)。每个请求——无论是否带浏览器标记——`Host` 都必须是回环地址权威,或与某个 `trustedHosts` 条目匹配:带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 `Origin` 并通过同一道比较。非浏览器客户端经由回环地址、部署推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,如附带 `Origin`,则它必须与 Host 权威完全一致;显式的 `sec-fetch-site: cross-site` 标记一律拒绝。不是纯的、规范形 `host[:port]` 权威的 `trustedHosts` 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载明确报错:否则解析会悄悄授权 `harness.internal/path` 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何事件流前拒绝握手。非回环组合必须显式信任其服务权威:Web 运行时从全接口服务器配置推导 LAN IP 字面量,cordis.yml 中的 `trustedHosts` 与 CLI(命令行界面)的 `--trusted-host` flag 则声明具名权威。`dsh web --host 0.0.0.0` 在远程访问具备认证层之前有意不受支持。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:[api 浏览器信任边界 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md)。
 
 ## `/api` WebSocket 下行
 

+ 2 - 2
packages/client/connection/src/api-request-trust.ts

@@ -7,8 +7,8 @@
  * nor Fetch-Metadata to reads (images and navigations — those
  * headers go only to trustworthy destinations), so an unmarked request may
  * still be a rebound browser read and Host is the one header rebinding cannot
- * forge. Non-browser and remote clients pass the same fence via loopback, the
- * CLI-derived LAN IP literals, or a declared `trustedHosts` authority.
+ * forge. Non-browser and remote clients pass the same fence via loopback,
+ * deployment-derived LAN IP literals, or a declared `trustedHosts` authority.
  * Network reachability and authentication stay out of scope: binding policy
  * belongs to the webserver config, and this fence is not an auth layer.
  */

+ 2 - 2
packages/client/connection/tests/node-half.host.spec.ts

@@ -199,8 +199,8 @@ describe('connection node half', () => {
     const loopback = fakeResponse()
     await routes[0]!.handler(fakeRequest({ host: '127.0.0.1:3080' }), loopback.response)
     expect(loopback.state.status).toBe(404)
-    // LAN authority declared as a port-less IP literal — the shape the CLI
-    // derives for `--host 0.0.0.0` — passes markerless curl on any port.
+    // An all-interfaces composition derives port-less LAN IP literals, which
+    // pass markerless curl on any port.
     const lan = fakeResponse()
     await routes[0]!.handler(fakeRequest({ host: '192.168.1.5:3080' }), lan.response)
     expect(lan.state.status).toBe(404)

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff