# Security Case holds logins. These are promises, with code you can read. - **The password is typed into the page, never handed to the agent.** Login injects via CDP `Input.insertText` (not keystrokes, not clipboard); the secret stays out of API responses, logs, and exec output. See `image/deskd.py`. - **deskd returns 423 while a credential is being injected.** Not just screenshots: `/exec`, `/action`, `/file` (read and write), `/eval`, `/auth/observe` and the capture reads all refuse until the injection finishes, and network capture drops anything in flight. An `/exec`, `/action` or `/eval` that was already running when the injection started returns 423 instead of its result, and a second login is refused (409) rather than overlapping. The password field is cleared (`CLEAR_PASS`) before the gate reopens, so the first screenshot after a login sees an empty box. - **That gate is "we do not hand it over", not "cannot obtain".** `computer_exec` runs bash in the same container as Chromium, and Chromium's CDP port is on that container's loopback. An agent that goes looking can reach what the browser holds. The 423 closes the paths Case itself offers; it is not a sandbox boundary. - **Login checks the live page before inserting credentials.** The host must match the credential's `domains`, and the scheme must be HTTPS. HTTP is allowed only on `localhost`, `127.0.0.1`, and `::1`. Password and verification-code entry repeat that check after redirects and between login steps. - **MCP has no credential-write tool.** Secrets enter via the Drive UI, `/fill`, or `bin/case cred add`. - **File API uploads are capped at 8 MiB.** cased and deskd count bytes as the body arrives, including requests without Content-Length. File reads enforce the same limit. - **`computer_upload` only assigns files already on the computer.** The path must be under `/home/agent/`, at most 5MB, and the snapshot ref must be `input[type=file]`. Password/OTP-like inputs are refused. Bytes travel through deskd `GET /file`, never command stdout. - **cased, Drive and MCP HTTP check `Host`, and `Origin` when one is present.** Anything else gets a 403 (MCP answers a bad `Host` with 421). Allowed by default: `127.0.0.1`, `localhost`, `[::1]`, the compose service name, plus `CASE_PUBLIC_HOST` and anything in `CASE_ALLOWED_HOSTS`. This is what stops a DNS-rebinding page or a cross-site WebSocket open from driving a loopback install, including a page calling `computer_exec` through `/mcp` on `127.0.0.1:8788`. Drive checks every request. cased checks the untokened ones — the token-in-URL doors always, everything when `CASE_TOKEN` is unset; with `CASE_TOKEN` set the rest of the API is bearer-only. - **cased binds loopback by default.** Compose publishes `127.0.0.1:8787` and `127.0.0.1:4174`. Set `CASE_TOKEN` before exposing those ports. - **MCP HTTP has no built-in bearer check.** Compose publishes port 8788 on loopback. Its `CASE_TOKEN` authenticates calls to cased, not requests from MCP clients. Keep that port local or put an authenticating reverse proxy in front. - **Desktops sit on their own Docker network (`case-desks`).** Compose joins only cased to both networks, so Drive and the MCP server have no route to a desktop. Desktops share `case-desks` with each other, so a desktop can still address its neighbours; what stops it there is the desk token, not the network — deskd and websockify both answer a neighbour with 401, and x11vnc listens on loopback only. Desktop containers publish no host ports under compose. - **The live desk is behind the desk token.** With `CASE_DOCKER_NETWORK` set, websockify runs under basic auth `agent:$DESK_TOKEN` (`image/start.sh`). Drive's `/live//…` is a proxy to cased `/v1/computers//live/…`, and cased adds that header; the WebSocket upgrade checks `CASE_TOKEN` itself, because Starlette's HTTP middleware never sees a websocket scope. Without `CASE_TOKEN`, that upgrade checks Host and Origin instead. Host mode (`bin/case up`) sets no network, so websockify is open there on the desktop's loopback-published port. - **`/fill`, `/assist` and `/answer` are doors opened by a token in the URL.** A human on a phone has no bearer header, so these three skip `CASE_TOKEN` and carry their own key: `/fill` and `/assist` links expire and are single-use, `/answer` is an HMAC over the handoff id. Anyone holding the URL is the human. Treat the links like one-time passwords. - **Audit log** (`~/.case/audit/.jsonl`): one line per API call with the caller's address, the query string, method, path, status and duration; request bodies that can carry secrets are redacted; response bodies are never logged. The three token doors log as `/fill/[token]` and friends, so the log records that a door was used without storing the key. - **ntfy handoff notifications carry a full-desktop PNG.** When the computer is awake, the screenshot rides along as the message attachment. Everyone subscribed to the topic sees whatever was on screen, which is the argument for a random topic name. Signed approval buttons require `CASE_PUBLIC_HOST` and an HTTPS reverse proxy; they are omitted without it. - **DeathByCaptcha gets scheme, host and path only.** The page URL is stripped of query and fragment before the solve request leaves (`control-plane/login_flow.py`), so the session tokens and continuation URLs a login page carries in its query string stay on the box. Sitekey, that URL and a configured proxy are all that go out. - **Drive screenshots and chat attachments persist on disk** under `~/.case/drive/shots` and `~/.case/drive/inbox` (Compose: `ui-data` via `CASE_HOME=/data`). They are content-addressed and kept until you delete the files or the volume. Deleting a thread does not erase them. Treat `~/.case/drive` / `ui-data` as sensitive chat material. Attachments never copy onto the computer; the model reads them from Drive. Max 4 files per turn, 5MB each; allowed types are PNG/JPEG/GIF/WebP, PDF, and text (including JSON, JS, XML). ## Self-host trust model When you run Case yourself, these assumptions matter: (a) **No CASE_TOKEN means an open API on the bind address.** Anything that can reach cased's or Drive's bind address can drive them. The Host/Origin check only rejects callers that address the box under a name it does not know; it is not authentication. Set `CASE_TOKEN` if the host is shared or ports are not loopback-only. (b) **A desktop can still reach cased.** Docker bridges are bidirectional: cased joins `case-desks` to dial the desktops, so a desktop can dial cased back on `:8787`. The Host check does not help, because a container writes its own `Host:` header. This does not require a compromise: anything running inside a desktop by design has that reach, including whatever the agent starts through `/exec` and any gateway installed into the box. The separate network keeps Drive and MCP out of a desktop's reach; it does not put cased out of reach, and it does not separate desktops from each other. `CASE_TOKEN` is the thing that stops a compromised desktop from driving the API, and each desktop's `DESK_TOKEN` is what stops it from driving its neighbours. This is a known limit, not a closed hole. (c) **`~/.case/` is secret-equivalent.** The Fernet encryption key lives beside the SQLite database. Treat the whole directory like a password manager export: back it up accordingly, restrict filesystem permissions, and do not copy it to untrusted storage. (d) **Desktops keep passwordless sudo by design.** The container is the agent's sandbox; `no-new-privileges` is deliberately not set because passwordless sudo requires setuid. Chromium also starts with `--no-sandbox`; the container is its isolation boundary. Do not run untrusted code inside a desktop you also use for personal browsing. (e) **ntfy topics and Telegram bot tokens are bearer secrets.** Anyone who knows a topic name can post or subscribe; anyone who holds the bot token can read and send as the bot. Treat both like passwords; use random topic names, and revoke the token in @BotFather if it leaks. Report vulnerabilities privately via GitHub Security Advisories: https://github.com/case-computers/case/security/advisories/new Do not file public issues that include tokens, ntfy topics, bot tokens, or vault contents.