OpenHands github.com/All-Hands-AI/OpenHands ↗

Open-source autonomous coding agent with Docker/Local/E2B runtimes. Web UI on port 3000 ships with no built-in auth. CVE-2026-33718 (git_handler shell injection, fixed 1.5.0). "Lethal Trifecta" GITHUB_TOKEN exfil published.

1

Audit Your Install and Pin a Known-Good Version

CVE-2026-33718 (command injection via get_git_diff()) was patched only in 1.5.0. Pull pinned images, never :latest.

docker pull docker.all-hands.dev/all-hands-ai/openhands:0.55 docker pull docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik openhands --version

Tip: subscribe to All-Hands-AI/OpenHands GitHub Security tab; diff config.template.toml between upgrades; rebuild runtime image after every bump (sandbox.force_rebuild_runtime = true).

2

Keep the UI Off the Public Internet

OpenHands' web UI binds to 0.0.0.0:3000 in the official docker run example and has no native login. Anyone who reaches the port owns your agent, your repos, and your LLM bill.

docker run -p 127.0.0.1:3000:3000 \ -e SANDBOX_USER_ID=$(id -u) \ docker.all-hands.dev/all-hands-ai/openhands:0.55

Tip: never publish :3000 directly; access over SSH tunnel, WireGuard, or Tailscale. If LAN access is required, set WEB_HOST and front it (next section).

3

Put an Auth Gateway in Front

Because there is no built-in user auth, terminate TLS and authenticate at a reverse proxy (nginx/Caddy/Traefik + OAuth2-Proxy, Cloudflare Access, or Tailscale Funnel + ACL). Also set a strong jwt_secret.

[core] jwt_secret = "$(openssl rand -hex 32)" # required, default ""

Tip: enforce SSO at the proxy, require WebSocket upgrade on /socket.io, rate-limit /api/conversations/*. Block all paths for unauthenticated users — the API has no second auth layer behind it.

4

Lock Down the Sandbox Runtime

The Docker runtime is the only boundary between the agent and your host. Run it as an unprivileged UID, off the host network, with no extra capabilities, minimal pinned base image. Avoid LocalRuntime outside disposable VMs.

[core] runtime = "docker" run_as_openhands = true [sandbox] base_container_image = "nikolaik/python-nodejs:python3.12-nodejs22-slim" user_id = 1000 use_host_network = false # critical enable_gpu = false timeout = 120 keep_runtime_alive = false rm_all_containers = true

Tip: consider RemoteRuntime (runtime.all-hands.dev), E2B, or Daytona for untrusted tasks. Never set use_host_network = true on a multi-tenant box.

5

Restrict Workspace Mounts and File Uploads

sandbox.volumes bind-mounts host paths into the container with the UID you chose — if you mount ~, the agent can read your SSH keys. Mount only the project directory, prefer :ro for anything you don't want rewritten.

[sandbox] volumes = "/srv/projects/acme:/workspace:rw,/srv/refs:/workspace/refs:ro" [core] workspace_base = "/srv/projects/acme" file_uploads_max_file_size_mb = 10 file_uploads_restrict_file_types = true file_uploads_allowed_extensions = [".py", ".ts", ".md", ".json", ".txt"] max_budget_per_task = 5.0 max_iterations = 100

Tip: never mount ~/.ssh, ~/.aws, ~/.docker, ~/.config/gh.

6

Restrict the Agent's Tool Surface

Each enabled tool is an attack primitive. Disable browsing if the task does not need internet (browser-rendered Markdown images were the exfil path in the GITHUB_TOKEN incident).

[core] enable_browser = false # kills the lethal-trifecta image vector [agent] enable_browsing = false enable_jupyter = false enable_llm_editor = false enable_cmd = true # bash - keep on, scope via sandbox enable_editor = true enable_prompt_extensions = false disabled_microagents = ["github", "npm"]

Tip: ship two profiles — coding.toml (no browser, no jupyter) and research.toml (browser on, no shell). Switch via --config-file.

7

Protect LLM Keys, OAuth Tokens, and Secrets

config.toml's [llm] api_key lands on disk in plaintext; conversation containers receive GITHUB_TOKEN / provider keys as env vars — exactly what the prompt-injection PoC exfiltrated. Inject secrets at runtime from a vault or --env-file.

chmod 600 ~/.openhands/config.toml docker run --env-file <(op inject -i secrets.env) ...
[llm] api_key = "${env:OPENAI_API_KEY}" base_url = "https://gateway.internal/openai/v1"

Tip: issue scoped GitHub tokens (single repo, no delete_repo, short TTL); rotate jwt_secret and all provider keys after any suspected injection.

8

Vet MCP Servers and Pin Them

OpenHands V1 reads ~/.openhands/mcp.json; any server can run arbitrary code (stdio) or call arbitrary HTTPS endpoints (http/sse). npx -y mcp-remote ... pulls current code from npm on every launch — supply-chain risk.

{ "mcpServers": { "tavily": { "url": "https://mcp.tavily.com/mcp/", "headers": { "Authorization": "Bearer ${TAVILY_KEY}" } } } }

Tip: pin versions (npx -y [email protected]), prefer vendored stdio binaries over npx/uvx, run openhands mcp disable <name> for anything unused, review each server's tool schema.

9

Use confirmation_mode + security_analyzer (Avoid Bare Headless)

In the web UI, set [security] confirmation_mode = true so the agent pauses before destructive actions. For CLI add a security_analyzer ("llm" or "invariant"). Headless mode ignores confirmation (always-approve) — never point it at untrusted tickets.

[security] confirmation_mode = true enable_security_analyzer = true security_analyzer = "invariant"

Tip: for CI, run headless only against trusted prompts; for human-in-the-loop sessions, keep confirmation on for run, write, browse, and any MCP tool call. Treat any security_risk: HIGH as auto-reject.

10

Defend Against Prompt Injection (Lethal Trifecta)

Published exfil chain: untrusted web content → agent renders Markdown image → URL contains base64-encoded ghp_… token → attacker server logs it. Mitigations are architectural, not promptcraft.

Real incident Embrace The Red demonstrated full GITHUB_TOKEN exfiltration from OpenHands via a poisoned web page → markdown image render → attacker URL. Same "Lethal Trifecta" pattern (read untrusted + privileged tools + exfil channel) hit Cline via DNS-encoded ping $(cat .env). OpenHands writeup · Cline writeup
  • Set enable_browser = false for any agent that touches secrets.
  • Run agents with either untrusted-content access or secrets access — never both.
  • Serve the UI with strict CSP img-src 'self' data: at the reverse proxy.
  • Strip Authorization, GITHUB_TOKEN, OPENAI_API_KEY from sandbox.runtime_startup_env_vars.
  • Keep conversations short-lived; rotate any token that ever entered an agent context.
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; connect-src 'self' wss:" always;

Tip: treat every fetched webpage, issue body, and MCP response as adversarial input.

References & further reading