# Security ## Reporting a vulnerability **Please do not open a public issue.** Use GitHub's private vulnerability reporting — the **Report a vulnerability** button under this repository's Security tab — or write to **security@attesttag.com**. Tell me what you did, what happened, and what you expected. A proof of concept helps enormously and does not need to be polished. I will acknowledge within a few days and tell you what I think the impact is; if I disagree that it is a vulnerability I will say why rather than going quiet. There is no bounty. I will credit you in the advisory for the fix unless you would rather I did not. ## Supported versions The latest release and `main`. This is a young project and there is no long-term support branch: fixes land on `main` and go out in the next release rather than being backported. ## What this software is responsible for Worth knowing before you go looking, because it shapes what counts as a vulnerability here. **It holds other people's credentials.** Connection secrets, Slack bot tokens, per-user OAuth tokens, TOTP secrets, single sign-on client secrets and organisations' own model keys are sealed with AES-256-GCM under `MASTER_KEY`. The model never sees them: a tool call goes through a proxy that injects the credential at the edge, over https only. Anything that gets a secret in front of the model, into a log, or across an organisation boundary is a serious finding. **It is multi-tenant.** One deployment can hold many organisations — the hosted service does — and the boundary between them is carried by every query. `TestEveryPerOrgQueryIsScoped` fails the build on a statement that touches a tenant's table without an organisation in its predicate, and `isolation_test.go` asserts the boundary holds for lists, reads by id, writes, attachments, sessions, alerts and spend. A path that crosses it is the highest-severity thing you can find. **It issues credentials that act as people.** Developer keys, and MCP connections made through its own OAuth (dynamic registration, PKCE, rotating refresh tokens), act as the person who made or approved them, with that person's role in that organisation, re-read on every request; only their hashes are stored. A token obtained without the person's consent, one that reaches past its person's permissions or organisation, or a spent refresh token that still works, is a serious finding. **It runs code the model wrote.** JavaScript from the model runs in QuickJS compiled to WebAssembly under wazero — no syscalls, no filesystem, no network except a host-provided fetch with its own budget. Fix jobs are different: they clone a repository and run *its* code, which is why the worker drops to an unprivileged user before doing so, and why its git runs as that same user so a config key the repository plants cannot execute as root. That split happens in every container mode; `WORKER_MODE=local` is a developer's subprocess and runs the repository's code as the bot's own user. The engine is given a model credential: a per-job key capped at the job's budget when `OPENROUTER_PROVISIONING_KEY` is set, otherwise the shared key (`WORKER_LLM_API_KEY`, else `OPENROUTER_API_KEY`) — set the provisioning key on any deployment that runs fix jobs and is open to signup, and the dispatcher refuses the shared key there. An organisation that brought its own model key and allowed fix jobs on it hands the worker that key, which cannot be capped per job. **It reads untrusted text.** Documents, web pages, Slack and Teams messages, mail forwarded into a channel and API responses all reach the model, and some of them will try to give it instructions. `docs/injection-test.md` is a deliberate prompt-injection payload used for testing, not a mistake — please leave it there. Prompt injection that causes the model to say something silly is expected. Prompt injection that reaches a credential, writes without the confirmation step, or crosses a tenant boundary is a vulnerability, and I want to hear about it. ## If you run this yourself Three things decide whether a self-hosted deployment is safe. The first two default to the careful answer; the third is yours to set: - **`MASTER_KEY` unlocks every stored credential.** Back it up somewhere other than the machine it runs on. Lose it and every workspace has to be connected again, every connection and stored key entered again, and everybody has to enrol their second factor again. The container image refuses to start without one rather than inventing one it would lose. - **`SIGNUP_MODE` defaults to `first-run`** — you sign up once and everybody else is invited. Set it to `open` only if you actually mean to run a service for strangers. - **Set `ADMIN_BASE_URL`** (or `PUBLIC_ORIGIN_HOSTS`) when you are behind a reverse proxy. Otherwise the first signed-in console request pins the public origin for the whole deployment, and that origin is what sign-in redirects, password-reset and invitation links are built from. Each organisation holds the bot to its own people with the email-domain list under Settings → Security, which starts as the domain its founder signed up with (unless that is a public mail provider); `ALLOWED_EMAIL_DOMAINS` is only the default for an organisation that keeps no list of its own. Even with neither, the bot refuses guests and members from other workspaces (Slack Connect, or another Microsoft 365 organisation) unless an admin lets them in on the same page.