fix(connection): fail the load on a trustedHosts entry that is not a bare authority

WHATWG parsing would quietly read a hostname out of harness.internal/path or
user@harness.internal, authorizing the typo's hostname; other typos would sit
silently ignored until requests 403. Refuse every URL part beyond host[:port]
at plugin load.
This commit is contained in:
creatixchu
2026-07-28 15:57:02 +08:00
parent 01eea07bab
commit b9cbe2f029
11 changed files with 67 additions and 12 deletions
@@ -40,6 +40,21 @@ function parseAuthority(authority: string): URL | undefined {
}
}
/**
* Assert one configured `trustedHosts` entry is a bare authority (`host` or
* `host:port`) and nothing else. WHATWG parsing would quietly read a hostname
* out of `harness.internal/path` or `user@harness.internal` — a typo must fail
* the load loudly instead of authorizing its hostname or being ignored until
* requests 403. The delimiter test refuses every URL part beyond the authority
* (path, backslash path, query, fragment, userinfo); IPv6 brackets use none of
* them.
* @param entry - the configured value, verbatim.
*/
export function assertTrustedAuthority(entry: string): void {
if (parseAuthority(entry) !== undefined && !/[/\\?#@]/.test(entry)) return
throw new Error(`client-connection: trustedHosts entry ${JSON.stringify(entry)} is not a bare host[:port] authority`)
}
/**
* Whether the request authority matches a `trustedHosts` entry. An entry with
* an explicit port matches that exact authority; a port-less entry matches the
+6 -2
View File
@@ -6,7 +6,7 @@ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
import { API_PATH } from './api-path.ts'
import { bridge } from './http-bridge.ts'
import { isTrustedApiRequest } from './api-request-trust.ts'
import { assertTrustedAuthority, isTrustedApiRequest } from './api-request-trust.ts'
export { API_PATH } from './api-path.ts'
@@ -23,7 +23,8 @@ export interface ConnectionConfig {
* port-less `host` matching any port. The /api trust fence refuses any
* browser request whose Host is neither loopback nor listed here, so a
* non-loopback (`0.0.0.0`) deployment must declare the names it is reached
* by (the dsh CLI derives the machine's LAN IP literals itself).
* by (the dsh CLI derives the machine's LAN IP literals itself). An entry
* that is not a bare authority fails the plugin load.
*/
trustedHosts?: string[]
}
@@ -42,6 +43,9 @@ export const Config: z<ConnectionConfig> = z.object({
export function apply(ctx: Context, config?: ConnectionConfig): void {
// The Loader resolves schema defaults; hand-built test contexts may pass none.
const trustedHosts = config?.trustedHosts ?? []
// Config boundary: a malformed entry fails the load loudly here rather than
// silently authorizing its hostname prefix at request time.
for (const entry of trustedHosts) assertTrustedAuthority(entry)
const apiHandler = toFetchHandler(ctx.apiProxy)
const route: WebRoute = {
kind: 'prefix',