diff --git a/.chronus/changes/fix-spector-stop-loopback-2026-7-16-12-15-0.md b/.chronus/changes/fix-spector-stop-loopback-2026-7-16-12-15-0.md new file mode 100644 index 00000000000..47f3be07419 --- /dev/null +++ b/.chronus/changes/fix-spector-stop-loopback-2026-7-16-12-15-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/spector" +--- + +Bind the mock server to the loopback interface (`127.0.0.1`) so the unauthenticated `/.admin/stop` endpoint can no longer be reached by other hosts on the network. The server is only reachable from the local host. diff --git a/packages/spector/docs/using-spector.md b/packages/spector/docs/using-spector.md index 96651da9bc6..bd62bb6a25f 100644 --- a/packages/spector/docs/using-spector.md +++ b/packages/spector/docs/using-spector.md @@ -34,6 +34,9 @@ tsp-spector serve ./path/to/scenarios --port 1234 tsp-spector serve ./path/to/scenarios --coverageFile ./path/to/spector-coverage.json ``` +> [!NOTE] +> The server always binds to the loopback interface (`127.0.0.1`), so it is only reachable from the local host and is not exposed to other machines on the network. + Alternative to start in background ```bash diff --git a/packages/spector/src/server/server.ts b/packages/spector/src/server/server.ts index 37570bd0eb7..274e728914b 100644 --- a/packages/spector/src/server/server.ts +++ b/packages/spector/src/server/server.ts @@ -10,6 +10,13 @@ export interface MockApiServerConfig { port: number; } +/** + * The mock server always binds to the loopback interface so it is only reachable + * from the local host. This keeps the unauthenticated admin endpoints (e.g. the + * server stop signal) from being exposed to other hosts on the network. + */ +const LOOPBACK_HOST = "127.0.0.1"; + const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => { logger.error("Error", err); @@ -85,14 +92,14 @@ export class MockApiServer { this.app.use(errorHandler); return new Promise((resolve, reject) => { - const server = this.app.listen(this.config.port, () => { + const server = this.app.listen(this.config.port, LOOPBACK_HOST, () => { const resolvedPort = getPort(server); if (!resolvedPort) { logger.error("Failed to resolve port"); reject(new Error("Failed to resolve port")); return; } - logger.info(`Started server on ${resolvedPort}`); + logger.info(`Started server on ${LOOPBACK_HOST}:${resolvedPort}`); resolve(resolvedPort); });