Summary
Agent0/executor_train/verl_tool/servers/serve.py binds the tool-execution server to 0.0.0.0:5000 by default, and POST /get_observation (serve.py:472-479) has no authentication dependency — the only dependency is a concurrency semaphore (serve.py:463-471). The endpoint forwards actions to tool backends including python_code (executes the action as Python), bash_terminal, ipython_code and sql. Any host that can reach the port can submit code without presenting any credential.
This is not just a theoretical default: the shipped launcher scripts hardcode it. eval_service/scripts/start_api_service.sh:3 sets host=0.0.0.0 and runs python -m verl_tool.servers.serve --host $host --port $port --tool_type "python_code" ..., and examples/train/math_tir/train_7b_grpo_megatron.sh:62 / train_mnodes.sh:52-60 / train_1.5b_grpo.sh:63 run the same server with --tool_type "python_code" bound to the host's primary address.
I confirmed the routing layer has no auth gate by starting the real AsyncToolServer class in-process (FastAPI TestClient, no real network bind) and sending an unauthenticated request: POST /get_observation with no Authorization header → 200, action processed, observations returned. GET /health → 200.
Details
serve.py:77-78 — host: str = "0.0.0.0", port: int = 5000 (defaults; main() at serve.py:604-605).
serve.py:463-471 — get_semaphore(): concurrency limiter only, no identity check.
serve.py:472-479 — @self.app.post("/get_observation") → Depends(get_semaphore) is the only dependency; handler routes to tool_manager.process_actions → per-tool execution (tools/python_code.py:456 executes the action; tools/bash_terminal.py, tools/ipython_code.py, tools/sql.py, tools/sandbox_fusion.py registered).
eval_service/scripts/start_api_service.sh:3 — host=0.0.0.0, --tool_type "python_code" (the code-exec tool), --workers_per_tool 32.
tools/python_code.py:21-52 — the only input filter is check_forbidden_imports, exact-string matching ("import subprocess", "os.system"); import subprocess (double space), __import__('sub'+'process'), importlib.import_module('subprocess') and getattr(os, 'system') all bypass it.
tools/python_code.py:193-235 — firejail is used only if installed; otherwise a bare [python_path, file] process with the full original environment, silently (no warning about the downgrade).
- Related surface (code-level; service not started locally):
eval_service/app.py:46-52 CORSMiddleware(allow_origins=["*"], allow_credentials=True); POST /completions + /chat/completions (app.py:75-107) with no auth; the vLLM api-key there is the hardcoded public token-abc123 (model_service.py:213); eval_service/config.py:44 binds 0.0.0.0.
How to reproduce
# as shipped (start_api_service.sh binds 0.0.0.0, python_code enabled):
curl -X POST http://<host>:<port>/get_observation \
-H 'Content-Type: application/json' \
-d '{"trajectory_ids":["t0"],"actions":["print(\"hello\")"]}'
# -> 200, no credentials required
Impact
Unauthenticated code execution from any reachable host on machines running the shipped training/eval launchers (the scripts bind the primary interface, not loopback). The tool backends execute Python actions with the server process's environment; without firejail there is no sandbox at all, and the filter is string-matching only.
Suggested change
- Default
host to 127.0.0.1 in serve.py and in start_api_service.sh / math_tir scripts (and document the remote case explicitly).
- Require a token for
/get_observation (the server already knows ServerConfig; add a bearer check) — or bind a unix socket.
- Replace the string-match filter with an allowlist/deny policy that cannot be bypassed by formatting, and fail loudly when firejail is unavailable instead of silently downgrading.
Summary
Agent0/executor_train/verl_tool/servers/serve.pybinds the tool-execution server to0.0.0.0:5000by default, andPOST /get_observation(serve.py:472-479) has no authentication dependency — the only dependency is a concurrency semaphore (serve.py:463-471). The endpoint forwards actions to tool backends includingpython_code(executes the action as Python),bash_terminal,ipython_codeandsql. Any host that can reach the port can submit code without presenting any credential.This is not just a theoretical default: the shipped launcher scripts hardcode it.
eval_service/scripts/start_api_service.sh:3setshost=0.0.0.0and runspython -m verl_tool.servers.serve --host $host --port $port --tool_type "python_code" ..., andexamples/train/math_tir/train_7b_grpo_megatron.sh:62/train_mnodes.sh:52-60/train_1.5b_grpo.sh:63run the same server with--tool_type "python_code"bound to the host's primary address.I confirmed the routing layer has no auth gate by starting the real
AsyncToolServerclass in-process (FastAPI TestClient, no real network bind) and sending an unauthenticated request:POST /get_observationwith no Authorization header →200, action processed, observations returned.GET /health→200.Details
serve.py:77-78—host: str = "0.0.0.0",port: int = 5000(defaults;main()atserve.py:604-605).serve.py:463-471—get_semaphore(): concurrency limiter only, no identity check.serve.py:472-479—@self.app.post("/get_observation")→Depends(get_semaphore)is the only dependency; handler routes totool_manager.process_actions→ per-tool execution (tools/python_code.py:456executes the action;tools/bash_terminal.py,tools/ipython_code.py,tools/sql.py,tools/sandbox_fusion.pyregistered).eval_service/scripts/start_api_service.sh:3—host=0.0.0.0,--tool_type "python_code"(the code-exec tool),--workers_per_tool 32.tools/python_code.py:21-52— the only input filter ischeck_forbidden_imports, exact-string matching ("import subprocess","os.system");import subprocess(double space),__import__('sub'+'process'),importlib.import_module('subprocess')andgetattr(os, 'system')all bypass it.tools/python_code.py:193-235— firejail is used only if installed; otherwise a bare[python_path, file]process with the full original environment, silently (no warning about the downgrade).eval_service/app.py:46-52CORSMiddleware(allow_origins=["*"], allow_credentials=True);POST /completions+/chat/completions(app.py:75-107) with no auth; the vLLM api-key there is the hardcoded publictoken-abc123(model_service.py:213);eval_service/config.py:44binds0.0.0.0.How to reproduce
Impact
Unauthenticated code execution from any reachable host on machines running the shipped training/eval launchers (the scripts bind the primary interface, not loopback). The tool backends execute Python actions with the server process's environment; without firejail there is no sandbox at all, and the filter is string-matching only.
Suggested change
hostto127.0.0.1inserve.pyand instart_api_service.sh/math_tirscripts (and document the remote case explicitly)./get_observation(the server already knowsServerConfig; add a bearer check) — or bind a unix socket.