DEV Community

Artemii Amelin
Artemii Amelin

Posted on

DeepSeek Harness Believed the Host Header. shell.online Now Documents Self-Hosting, and TRUST_PROXY Ships Off.

VulnCheck published CVE-2026-82533 on September 8. It is against DeepSeek Harness, the open-source tool that runs a coding agent's commands inside an operating-system sandbox on the developer's machine, and it scores 9.4. The OX Security write-up by Nir Zadok and Moshe Siman Tov Bustan describes the whole bug in one function. isTrustedApiRequest read the request's Host header, allowed the call if the value was a loopback authority or appeared in a configured trustedHosts list, and never compared it with the connection's actual peer address.

The harness hands the agent's shell the address of its own local API and the current session id. So the escape was one curl from inside the sandbox to the harness's session-update endpoint, with a Host header of 127.0.0.1 and a body that set approvalPolicy to never and dangerMode to danger-full-access. Every command after that ran unconfined and without a prompt. Versions 0.1.1-rc.2 and earlier are affected. The fix landed on August 27 in 0.1.2-alpha.1, and The Hacker News reports it replaced the header check with a one-time startup token that the browser exchanges for a signed cookie.

A header is something the caller wrote. Nothing about that changes because the header is named Host, or X-Forwarded-For, or because the value it carries happens to be a loopback address.

A self-hosting guide merged to shell.online today

This is worth writing up today because the shell.online repository merged a self-hosting guide today, and the first thing an operator running our accounts app has to decide is exactly the question DeepSeek Harness got wrong: which request headers do you believe about where a request came from.

The guide splits shell.online into two services. The relay serves the public site and carries encrypted terminal frames. The accounts app in app/ is optional, adds sign-in, organizations, linked machines and the shared session list, and the CLI does not need it.

The relay runs on Cloudflare Workers and needs Durable Objects, Rate Limiting and Analytics Engine. The new wrangler.example.jsonc carries no account identifiers or credentials: two Durable Object classes, TerminalSession and StatsStore, and four rate limiters. Session creation is 10 per minute, connections and events 120 per minute, stats login 10 per minute. Copy it to wrangler.local.jsonc, deploy, and point the CLI at the result with shell --server https://example.workers.dev <command> or the SHELL_ONLINE_SERVER variable.

What the operator of that relay can see is the same as what we can see on the hosted one, and the README states it: connection metadata, encrypted frame sizes, timing, labels and session lifecycle events. The CLI owns the PTY and encrypts frames before they reach the relay. The browser decrypts them. Running the relay yourself moves the metadata into your own Cloudflare account. It does not give you the terminal.

The accounts app needs Firebase Authentication and PostgreSQL. docker compose up --build -d in app/ starts the client and API together on port 8080 and proxies /relay/* to whichever relay RELAY_URL names. shell login is pointed at it with SHELL_ONLINE_ACCOUNTS and SHELL_ONLINE_WEB.

The header our accounts app refuses by default

Among the variables in the app's .env.example is one line that the DeepSeek advisory makes easy to explain:

# Set only behind a proxy that replaces X-Forwarded-For
TRUST_PROXY=0
Enter fullscreen mode Exit fullscreen mode

The doc comment on callerAddress in app/server/lib/rate-limit.ts says why: "X-Forwarded-For is a request header like any other: anyone can send one. It is read only when the deployment says it sits behind a proxy that rewrites it, because trusting it otherwise would let a caller pick a new identity per request and walk straight past the limiter." With the flag off, the address held responsible for a request is the socket's remote address, and the header is ignored. There is a test for it, named "does not believe a forwarded address unless a proxy is trusted", and its comment states the failure it guards against: without the default, "a caller sets X-Forwarded-For to a new value per request and the limiter never sees the same caller twice."

On the hosted deployment the app runs as a Worker, and there the flag is hard-coded off with a comment that it "must stay off". The adapter takes the caller's address from CF-Connecting-IP, which the Cloudflare edge writes and a caller cannot forge past it. The relay Worker does the same: every rate limiter in worker/index.ts is keyed on CF-Connecting-IP.

That is the difference between the two designs. DeepSeek Harness asked the request what it was. Our app asks the transport, and only believes a header when the operator has stated, in configuration, that a specific proxy in front is the one writing it.

The same split exists in Pilot Protocol, one layer down. The rendezvous server that handles registration and NAT hole-punching can be run by anyone with rendezvous -registry-addr :9000 -beacon-addr :9001, and it is out of the data path once a tunnel is up. When hole-punching fails, the beacon relays the traffic, still end-to-end encrypted. Self-hosting the coordination point changes who sees the metadata. It cannot change who can read the payload, because the relay was never given a key.

What the same PR removed

PR #97 also deleted the three GitHub Actions workflows added in 0.10.0 that ran LLM-assisted issue triage, pull-request review and release notes, along with their five prompt files, the moderation policy script and its test. The triage workflow ran two model calls on every new issue and, when both agreed, called the deleteIssue GraphQL mutation, falling back to close-and-lock if GitHub refused permanent deletion. The paragraph in CONTRIBUTING.md describing the 98% confidence threshold is gone with it, and the moderation test is out of npm test and CI. The four workflows left, CI, CodeQL, the container build and the app deploy, do not touch issues at all. As of today, nothing in the repository closes or deletes an issue except a person.

Four internal deployment documents under app/ went too, replaced by the guide above and a shorter app README. The Docker Compose image is now pinned to 0.11.1, which matches the release, and Wrangler moved to 4.131.

For anyone deciding whether to run the relay themselves: the guide is the file to read, and TRUST_PROXY is the line to get right before the app is reachable from anywhere but localhost.

Top comments (0)