fix(bash-guard): deny internal curl to orchestrator/localhost

Prompts told agents internal API calls were denied; the guard only
denied GitHub. Combined with task 4 (X-Agent-Role enforcement) this
closes the manifest-bypass loophole.
This commit is contained in:
Renn F
2026-05-03 06:11:38 +02:00
parent 38246050d6
commit 8381ade3ce
3 changed files with 69 additions and 0 deletions
+17
View File
@@ -72,6 +72,23 @@ if echo "$low" | grep -qE '(^|[[:space:];&|])(curl|wget|http|https)[[:space:]][^
exit 2
fi
# --- internal API calls -------------------------------------------------------
# Agents must reach the orchestrator through their MCP manifest verbs, never
# raw HTTP. This blocks `curl http://roboco-orchestrator:8000/...`,
# `wget http://localhost:8000/...`, `http http://127.0.0.1/...` (HTTPie),
# scheme-less forms like `curl roboco-orchestrator:8000/...`, and the
# protocol-relative `//host/path` form.
# KNOWN GAPS (out of scope here):
# - Variable expansion: `URL=http://orchestrator/x; curl $URL` — the guard
# sees `curl $URL`, not the expanded URL, so this slips through. The
# X-Agent-Role check (task 4) is the second gate.
# - Interpreter one-liners: `python -c "import urllib.request; ..."` — too
# deeply hidden to regex. Mitigated by the manifest-bound MCP surface.
if echo "$low" | grep -qE '(^|[[:space:];&|])(curl|wget|http|https|httpie)[[:space:]][^|]*((http|https):)?//?(roboco-[a-z0-9_-]+|localhost|127\.0\.0\.1|0\.0\.0\.0)[:/]'; then
echo "Denied: internal API calls bypass the gateway. Use the MCP verbs (roboco-flow / roboco-do / roboco-git-readonly / roboco-optimal / roboco-docs) — they route through the orchestrator with the right auth and tracing." >&2
exit 2
fi
if echo "$low" | grep -qE '(^|[[:space:];&|])(env|printenv)([[:space:]]|$)' && ! echo "$low" | grep -qE '(^|[[:space:];&|])env[[:space:]]+-i'; then
# allow `env VAR=val cmd` style prefixes (`env ` followed by `NAME=`)
if ! echo "$low" | grep -qE '(^|[[:space:];&|])env[[:space:]]+[a-z_][a-z0-9_]*='; then
View File
+52
View File
@@ -0,0 +1,52 @@
"""bash-guard-hook.sh denies curl/wget against orchestrator/localhost.
The PreToolUse hook reads Claude Code's event JSON from stdin in the form
``{"tool_name": "Bash", "tool_input": {"command": "..."}}`` and exits with
code 2 to deny, 0 to allow. Tests wrap each command in that envelope.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
# tests/unit/scripts/test_bash_guard.py
# parents[0] = scripts
# parents[1] = unit
# parents[2] = tests
# parents[3] = <repo root>
GUARD = Path(__file__).parents[3] / "docker" / "scripts" / "bash-guard-hook.sh"
_DENIED = 2
_ALLOWED = 0
def _run(cmd: str) -> int:
payload = json.dumps({"tool_name": "Bash", "tool_input": {"command": cmd}})
result = subprocess.run(
[str(GUARD)],
input=payload,
capture_output=True,
text=True,
)
return result.returncode
def test_blocks_internal_curl_to_orchestrator() -> None:
assert (
_run("curl http://roboco-orchestrator:8000/api/v2/flow/main_pm/delegate")
== _DENIED
)
def test_blocks_internal_curl_to_localhost() -> None:
assert _run("curl http://localhost:8000/api/v2/flow/dev/i_am_done") == _DENIED
def test_blocks_internal_curl_to_127() -> None:
assert _run("curl http://127.0.0.1:8000/api/health") == _DENIED
def test_allows_external_curl_to_documentation() -> None:
assert _run("curl https://docs.python.org/3/") == _ALLOWED