# Threat model What Sara protects, from whom, and how. It was first written for Phase 24 (0.24.0) after a full security review (sign-in, demo, sandbox and tools, credentials and personal data, phone apps and web front-end), and updated for 1.0 (Phases 46-54: the installers for Linux, Windows and macOS, history import, device sign-in for apps, previews and the testing browser, the code map). Keep it current when a feature changes how data or authority flows. Some 1.0 hardening is described here as the intended 1.0 behaviour while it is being finished: it is marked **(1.0)**. ## What there is to protect | Asset | Where it lives | Why it matters | |---|---|---| | Conversations, memories, notes, planner, scrapbook, look pictures | `sara.db` and files in the data folder | The owner's private life | | Saved places (name and coordinates, Phase 29) | `sara.db` | Where the owner goes; the phone never sends its live location, only "arrived at / left place N" | | Third-party credentials: GitHub token, Home Assistant token, Google OAuth tokens, provider API keys, subscription sign-ins (ChatGPT, Claude, Grok), APNs key, bridge tokens | The encrypted vault (AES-256-GCM, master key `0600` in the data folder or `SARA_MASTER_KEY`); agent sign-ins under `agents/` | Access to the owner's other accounts | | The owner's sign-in: owner token (stored as a hash), session secret, passkeys, two-step secret | `owner.json` (`0600`) | Whoever has these *is* the owner | | Backups | `backups/` and the off-site copy | They hold everything above, including the vault key unless a passphrase is set | | The server itself | **Linux:** Sara runs as the unprivileged `sara` user. **Docker:** as an unprivileged user in the container. **Windows service:** as its own limited account `NT SERVICE\Sara` (not SYSTEM). **Windows "only while I'm signed in"** and **macOS:** as the signed-in owner's own account | Sara never has root or administrator rights. In the Windows signed-in mode and on macOS the server process can read everything that user can (their documents, browser profiles, keys), so a compromise of Sara there is a compromise of that account; the command sandbox (macOS) and approvals (Windows) are what stand between her tools and those files | ## Who might attack, and what they can do 1. **Someone on the internet** who can reach the server but has no sign-in. 2. **A demo visitor** on a `SARA_DEMO=1` server. Every visitor is technically signed in. 3. **Someone who gets a session cookie**, for example from an unlocked device left signed in. 4. **Content the model reads**: web pages, emails, GitHub issues and pull requests, files in a cloned repository, MCP server output. That content can try to steer the model (prompt injection). The model itself is treated as untrusted when it asks for tools. 5. **A hostile repository** that the owner asks Sara to work on: its git config, hooks and links. 6. **The `sara` user itself**, if the app is ever compromised. It must not be able to become root. 7. **A malicious app or site on the owner's phone** talking to the Sara app. Out of scope: someone with root on the server, and someone who has the owner's unlocked computer and owner token. Model providers see what is sent to them; private conversations are sent only to a provider on hardware the owner controls. ## Defences, by attacker ### 1. Internet, no sign-in - Every `/api` route needs a session cookie, the owner token or a device token, except: health, login, pairing redemption, passkey sign-in, `GET /api/session` (says only whether you're signed in), the device sign-in start and poll (`POST /api/device/start`, `/api/device/poll`: they hand out a code and, once the owner approved it, that app's token), the Google sign-in return (`GET /api/google/callback`, checked against the one-time state it started with), the demo start, the privacy and support pages, and `POST /api/public/report`, which exists only on the developer's report inbox (`SARA_REPORT_INBOX=1`, size- and rate-limited). Previews under `/preview//` need no session: the random token is the authorisation (section 4b). With two-step sign-in on, the owner token alone is not enough. - The owner token, pairing codes (10 minutes, one use) and two-step codes are compared in constant time and stored hashed. Wrong attempts are limited per address (IPv6 per /64). Passkey challenges are one-time and have their own allowance for sign-in and for adding a key. - The session cookie is `HttpOnly; SameSite=Strict`, and `Secure` on https (automatically behind a trusted proxy). Responses send a strict CSP (no inline scripts or styles), `frame-ancestors 'none'`, `nosniff`, `Permissions-Policy` (microphone and, since Phase 69's video calls, the camera for the app's own page only; nothing for previews), `COOP`/`CORP same-origin`, and HSTS on https. Camera stills from a video call are size-checked JPEGs, rate-limited per call, kept in memory only and refused in the demo (docs/VIDEO_CALLS.md). - Requests that change things must be JSON and must come from the same site. Sign-out and the demo start check this too, so a cross-site form can't sign the owner out or plant a session. ### 2. Demo visitors - A visitor's conversations live in memory and are visible only to that visitor. Reports are filtered per visitor. - Visitors can **read** only an allowlist of routes (`DEMO_READS` in `src/server.ts`) and write only a short list (`DEMO_WRITES`). Backups, workspace files, health details, accounts, devices, updates, exports and character files are refused. A test lists them. - The demo makes no backups, has no live calls, and its owner token is not used anywhere public. ### 3. A stolen session cookie - Adding or removing a passkey and starting two-step sign-in need the owner token, or the current code when two-step is on. A stolen cookie can't create a lasting way back in. - Turning two-step on signs every other device out. A new owner token signs every device out and, by default, removes passkeys. - Settings → Data & health → Devices and sign-in lists every signed-in device and app, with sign out for each. - **(1.0)** Approving a new app or device needs re-authentication (the owner token, a current two-step code or a passkey), so a stolen cookie alone can't add a lasting device. Device tokens can't manage devices or sign-in settings. ### 4. Content the model reads (prompt injection) - Text from web pages, email and GitHub is labelled as information, never instructions. - Tools that send, write outside the workspace, spend money or reach other people ask the owner first (an approval card). Reading tools run alone. Only tools offered for that turn can run, whatever the model asks for; partner chats have no tools. - Private conversations use no off-server tools (web, mail, calendar, GitHub, MCP, delegation, push) and their commands get no network. **(1.0)** Live voice calls are refused on private conversations (a call streams to a cloud voice service). - `web_fetch` refuses private, loopback, link-local (cloud metadata), CGNAT and reserved addresses, including the IPv6 spellings that carry an IPv4 address inside, and checks again after each redirect. - Output from tools is scrubbed of anything that looks like a stored secret before it reaches the model or the chat. ### 4b. Web apps she builds (previews and the testing browser, Phase 49) What runs in a preview is code she (or a repository) wrote, so it is treated as hostile. - Previews are served only under `/preview/<128-bit random token>/`. The token is the only authorisation (never the session cookie). It is made by her `preview_start` tool or the owner-only `/api/previews`, bound to one workspace, one port and the background job that opened it, ends after 12 hours, when the job ends, or when revoked, and answers 404 otherwise. - The port must be listened on by a process of that job's own process tree, checked with the operating system (`/proc` on Linux, netstat + the process list on Windows, lsof on macOS); what the job prints is only a hint. Sara's own port is refused; only 127.0.0.1 is ever contacted. **(1.0)** Only IPv4 loopback listeners owned by the job are proxied (not `::1`, not a listener on another address that happens to answer on loopback). - Every response carries `Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads` (no `allow-same-origin`): the page has an opaque origin, so its scripts can't read Sara's cookies or storage and its requests to `/api` carry no session (SameSite=Strict) and can't be read (no CORS). The chat shows it in an `