/** * Who is allowed to talk to the dashboard, and how they prove it. * * THE SHAPE OF THE PROBLEM. This server holds an OrcaRouter API key and can install or delete one * over HTTP. Until now it bound every interface (`app.listen(PORT)` with no host), answered with a * wildcard `Access-Control-Allow-Origin`, and required no authentication of any kind. Anything on * the same network could drive it, and so could any web page the user happened to visit. * * Three defences, because each one alone has a documented way past it: * * 1. BIND TO LOOPBACK. Removes the network reach. Not sufficient on its own: a page in the user's * own browser is already on loopback as far as the socket is concerned. * * 2. A LITERAL HOST ALLOWLIST. This is the DNS-rebinding defence. An attacker points * evil.example at 127.0.0.1, so the connection genuinely arrives on loopback and the browser * genuinely considers it same-origin -- `Origin` and `Host` agree, and any check that compares * them to each other passes. What the attacker cannot do is make `Host` read `localhost`, so * the comparison is against a fixed list rather than against the request itself. webpack-dev- * server and Jupyter both added exactly this, each after a CVE. * * 3. A CAPABILITY TOKEN. The strongest of the three, and the one that makes the others belt and * braces. A secret is generated per run, written 0600, and embedded in the HTML this server * returns. Mutating requests must echo it in a header. A cross-origin page cannot read that * HTML -- it is not a CORS-readable response -- so it cannot learn the token, and a request it * forges is refused even when the origin and host look perfect. This is Jupyter's model. * * WHAT IS DELIBERATELY NOT HERE. No password, no login, no session. The trust boundary is the * filesystem: whoever can read a 0600 file in the user's own home is already the user. */ import type { NextFunction, Request, Response } from 'express'; /** The header a browser must echo. Non-simple, so a cross-origin form post cannot set it either. */ export declare const TOKEN_HEADER = "x-token-optimizer-dashboard"; /** * Is the peer on this machine? * * THE BIND ADDRESS IS NOT THE QUESTION. An earlier version of this module keyed the token on * `isExposed()`, i.e. on how the socket was opened, which meant that the moment someone set * TOKEN_OPTIMIZER_DASHBOARD_HOST the page -- token and all -- was served to anybody who asked for * it. The token is a capability; handing it to an unauthenticated network client defeats every * other defence here, and it defeated the one the startup warning claimed was still standing. * * So the question is who is CONNECTING, answered per request from the socket rather than from * configuration. IPv4-mapped IPv6 (`::ffff:127.0.0.1`) is how a dual-stack listener reports a * loopback peer, so it counts too. */ export declare function isLoopbackPeer(req: Request): boolean; /** * The interface to bind. * * LOOPBACK BY DEFAULT, which is the change in behaviour: a dashboard that was reachable from the * LAN no longer is. The override exists because someone genuinely does run this on a box they reach * from elsewhere, and silently breaking them would be its own failure -- but they have to ask. */ export declare function dashboardHost(env?: NodeJS.ProcessEnv): string; /** True when the bind address is not loopback, i.e. the user opted into network exposure. */ export declare function isExposed(env?: NodeJS.ProcessEnv): boolean; /** * Reject a request whose `Host` is not one of ours. * * Skipped when the operator opted into network exposure, because then a real hostname or LAN * address is the point and this check would refuse every legitimate request. */ export declare function hostGuard(env?: NodeJS.ProcessEnv): (req: Request, res: Response, next: NextFunction) => void; /** Where the per-run secret lives. Under the optimizer's own home, never the workspace. */ export declare function tokenPath(env?: NodeJS.ProcessEnv): string; /** * The capability token, created on first use. * * Written before it is returned, and re-chmod'ed afterwards: a file that already existed keeps its * old mode through a plain write, so creating it 0600 is not enough on its own. */ export declare function capabilityToken(env?: NodeJS.ProcessEnv): string; /** Only for tests: forget the cached token so a different home can be exercised. */ export declare function resetCapabilityToken(): void; /** * Require the token on anything that changes state. * * Reads stay open: they are already reachable by anyone who can reach the port, and gating them * would break the dashboard's own first paint, which fetches before it has run any script. */ export declare function requireCapability(env?: NodeJS.ProcessEnv): (req: Request, res: Response, next: NextFunction) => void; /** * The origins allowed to read a response. * * Replaces a bare `cors()`, whose wildcard let ANY site read everything this server returns -- * including the provider status, which names the account a key belongs to. Only the dashboard's own * loopback origins are reflected, and only when the bind is loopback. */ export declare function corsOrigin(env?: NodeJS.ProcessEnv): (origin: string | undefined, done: (e: Error | null, ok?: boolean) => void) => void; /** * The token, handed to the page that is allowed to have it. * * Injected into the HTML this server returns, which a cross-origin page cannot read. The wrapper * adds the header to same-origin API calls so no individual `fetch` site has to remember, and so a * call added later cannot silently miss it. */ export declare function injectToken(html: string, env?: NodeJS.ProcessEnv, req?: Request): string; //# sourceMappingURL=dashboard-guard.d.ts.map