fix(cli): sample LAN addresses once — trust and the printed LAN URL share the snapshot
web.ts re-sampled interfaces after boot, so an address change during entry.run() could advertise a LAN URL absent from the trustedHosts snapshot composePatches captured, answering 403 on arrival. resolveLanTrust now returns the single sample and AppCLIEntry exposes it for display.
This commit is contained in:
@@ -34,28 +34,31 @@ export const ALL_INTERFACES_HOST = '0.0.0.0'
|
||||
* authorities an all-interfaces bind is reachable by on the LAN.
|
||||
* @returns the addresses in interface order (possibly empty).
|
||||
*/
|
||||
export function lanIPv4Addresses(): string[] {
|
||||
function lanIPv4Addresses(): string[] {
|
||||
return Object.values(networkInterfaces()).flat()
|
||||
.filter((iface): iface is NonNullable<typeof iface> => iface !== undefined && iface.family === 'IPv4' && !iface.internal)
|
||||
.map(iface => iface.address)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorities the /api browser-trust fence must accept for one invocation:
|
||||
* the machine's LAN IP literals when the effective bind is all-interfaces
|
||||
* (advertised by the printed LAN URL, so they must not answer 403), followed
|
||||
* by the explicit extras. Derived entries are port-less IP literals — DNS
|
||||
* rebinding needs an attacker-controlled name, so an IP-literal Host is safe
|
||||
* on any port, and the bound port may be OS-assigned, unknowable pre-boot.
|
||||
* One LAN-trust resolution for one invocation, sampled exactly once: the
|
||||
* machine's LAN IP literals when the effective bind is all-interfaces, and
|
||||
* the `trustedHosts` value built from them plus the explicit extras. The
|
||||
* single sample is deliberate — display must advertise only addresses the
|
||||
* fence was configured with, so both read this snapshot. Derived entries are
|
||||
* port-less IP literals: DNS rebinding needs an attacker-controlled name, so
|
||||
* an IP-literal Host is safe on any port, and the bound port may be
|
||||
* OS-assigned, unknowable pre-boot.
|
||||
* @param bindHost - the effective webserver bind host (CLI flag, else the yml default).
|
||||
* @param extra - `--trusted-host` values, in argv order.
|
||||
* @returns the connection row's `trustedHosts` value (possibly empty).
|
||||
* @returns the sampled LAN addresses and the connection row's `trustedHosts` value (each possibly empty).
|
||||
*/
|
||||
export function resolveTrustedHosts(bindHost: string | undefined, extra: readonly string[]): string[] {
|
||||
return [
|
||||
...bindHost === ALL_INTERFACES_HOST ? lanIPv4Addresses() : [],
|
||||
...extra,
|
||||
]
|
||||
export function resolveLanTrust(
|
||||
bindHost: string | undefined,
|
||||
extra: readonly string[],
|
||||
): { lanAddresses: string[]; trustedHosts: string[] } {
|
||||
const lanAddresses = bindHost === ALL_INTERFACES_HOST ? lanIPv4Addresses() : []
|
||||
return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] }
|
||||
}
|
||||
|
||||
/** One profile-json key mapped onto a yml row's config field. */
|
||||
@@ -126,6 +129,14 @@ export class AppCLIEntry {
|
||||
/** The root context, set by {@link run}. */
|
||||
ctx!: Context
|
||||
|
||||
/**
|
||||
* LAN IPv4 addresses sampled once at patch composition — the exact snapshot
|
||||
* the /api trust fence was configured with. Display reads this instead of
|
||||
* re-sampling, so the advertised LAN URL can never name an address the
|
||||
* fence rejects. Empty unless the effective bind is all-interfaces.
|
||||
*/
|
||||
lanAddresses: readonly string[] = []
|
||||
|
||||
private patches: PatchOptions[] = []
|
||||
|
||||
constructor(private readonly options: AppCLIEntryOptions) {}
|
||||
@@ -188,9 +199,10 @@ export class AppCLIEntry {
|
||||
if (this.options.workspaceRoot !== undefined) put('api-gateway', 'workspaceRoot', this.options.workspaceRoot)
|
||||
|
||||
// Source 2b: authorities for the /api browser-trust fence (rationale on
|
||||
// resolveTrustedHosts).
|
||||
// resolveLanTrust).
|
||||
const ymlHost = (rows.get('webserver')?.config as { host?: string } | undefined)?.host
|
||||
const trustedHosts = resolveTrustedHosts(this.options.host ?? ymlHost, this.options.trustedHosts ?? [])
|
||||
const { lanAddresses, trustedHosts } = resolveLanTrust(this.options.host ?? ymlHost, this.options.trustedHosts ?? [])
|
||||
this.lanAddresses = lanAddresses
|
||||
if (trustedHosts.length > 0) put('connection', 'trustedHosts', trustedHosts)
|
||||
|
||||
// Source 3: the frontend dist — an assembly fact of this app, never yml
|
||||
|
||||
Reference in New Issue
Block a user