Ctrl K
ComunidadGuías…

SECURITY.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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/<id>/…` is a proxy to cased `/v1/computers/<id>/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/<date>.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.