# Sparrow threat model Sparrow is a self-hosted webhook delivery service written in Go. Producers push events to a REST API (`/v1/*`, Huma, OpenAPI 3.1); Sparrow fans each event out to the subscriptions of a *consumer*, optionally reshapes the payload with a Go template, signs it, and POSTs it to the consumer's registered webhook URLs with retries, pausing, health tracking and auto-disable. PostgreSQL holds all state and the job queue (River). An embedded Svelte SPA and a consumer-facing portal are served by the same binary. Read `AGENTS.md` and `okf/index.md` for the current architecture; do not assume package names are fixed. The typical deployment is one organisation running Sparrow behind a VPN, with several internal teams (consumers) sharing it. It is not designed as public multi-tenant SaaS, but the API and portal may be reachable from untrusted networks and the consumer boundary is a real security boundary. ## Principals and trust - **Operator**: controls environment variables, the keyring (`SPARROW_ENCRYPTION_KEYS`), the database and the host. Trusted. - **Master key holder** (`SPARROW_API_KEY`): full API access. Trusted, but the key must not leak (logs, responses, the served UI). - **Tenant-wide access token**: issued via `/v1/tokens` or invites; acts on all consumers. Must be revocable, expire as configured, and never exceed the scope it was minted with. - **Consumer-scoped token / portal bearer**: lower trust. Refused on `/v1` (403); only valid through the portal gateway `/portal/api/*`, which re-dispatches to `/v1` pinned to that one consumer. **Any way for such a token to read or change another consumer's webhooks, subscriptions, events, deliveries, secrets or templates, or to reach `/v1` directly, is high or critical.** - **Unauthenticated network clients**: may reach `/health`, `/ready`, `/metrics`, `/docs`, `/openapi.*`, the UI, and `POST /invite/redeem`. - **Webhook receivers**: untrusted remote servers. Their responses, status codes, redirects, headers, timing and TLS behaviour are attacker-controlled. - **Event producers**: event payloads, event type names, schemas and consumer names are untrusted input even from an authenticated caller. - When `SPARROW_API_KEY` is unset all endpoints are open; that is documented and not a finding by itself. ## Where untrusted input enters - REST handlers in `internal/rest/` and auth/portal/CORS middleware in `internal/middleware/` (`auth.go`, `portal_gateway.go`), the token/invite library in `pkg/access/` and its adapter `internal/accessauth/`. - Webhook target URLs: SSRF protection in `internal/webhooks/urlvalidation.go` and `internal/webhooks/client/ssrf.go` (loopback, private ranges, cloud metadata blocked unless `SPARROW_ALLOW_PRIVATE_NETWORKS` or `SPARROW_ALLOWED_NETWORKS` allow them). Check DNS rebinding, redirects, IPv6 and IPv4-mapped forms, alternate IP encodings, and that the check applies at connect time, not only at registration. - Transform templates (`pkg/template/`, a custom `text/template` FuncMap) are authored by API callers, including portal users for their own consumer. Template execution must not read files, env vars or secrets, reach the network, or exhaust the server (CPU, memory, unbounded output). - Event type schemas and bundles (`/v1/event-types:import`), JSON payloads, cursor tokens, filters and query parameters that reach SQL in `internal/webhooks/store/`. - AI template drafting (`internal/ai/`, off unless configured): payload samples are sent to the configured provider; returned drafts are verified before use. - The embedded UI and portal (`web/`): stored XSS via consumer names, event payloads, templates, delivery bodies or receiver responses rendered in the UI. ## Security properties - Consumer isolation through the portal gateway and consumer-scoped tokens; the reserved `_sparrow` system consumer cannot be targeted by consumer tokens or receive non-`sparrow.*` subscriptions. - Webhook signing secrets and other sensitive fields are envelope-encrypted (`pkg/crypto/`, AES-256-GCM) at rest and never returned in plaintext beyond what the API documents. Outbound signatures (`pkg/signature/`) must be correct and verifiable; verification helpers must be constant-time. - Access tokens and invites: single use where documented, expiry, revocation, and prefix/realm checks must hold; hashes, not tokens, are stored. - Deliveries never go to addresses the SSRF policy forbids. - Soft-deleted webhooks (`deleted_at`) must not receive deliveries or be readable as live resources. ## How to exercise it From a checkout, `make dev-db` starts a throwaway PostgreSQL matching the default `DATABASE_URL`, and `make run` fills in a dev-only all-zeros keyring (`scripts/dev-env.sh`). ```sh make dev-db make test # all workspace modules go test ./internal/middleware/... ./pkg/access/... SPARROW_API_KEY=master make run & # migrates on start, listens on :8080 curl -s -H 'X-API-Key: master' localhost:8080/v1/whoami cd web && npm run test:unit ``` Use local HTTP servers as webhook receivers; with the default SSRF policy you need `SPARROW_ALLOWED_NETWORKS=127.0.0.1/32` (or `SPARROW_ALLOW_PRIVATE_NETWORKS=true`) to deliver to localhost. Integration (`-tags integration`, testcontainers), Gauge e2e and Playwright tests also need Docker or browsers. ## Severity - Critical: unauthenticated access to `/v1` when a key is configured; RCE; a consumer-scoped token gaining tenant-wide access; decryption of stored secrets without the keyring. - High: cross-consumer read or write through the portal; SSRF bypass reaching loopback, private ranges or cloud metadata under the default policy; SQL injection reachable by any authenticated caller; stored XSS in the UI or portal triggered by consumer- or receiver-controlled data; token or invite reuse/expiry bypass; signature forgery. - Medium: secret or token disclosure in logs or error messages; template execution that exhausts the server; denial of service by one authenticated consumer that stalls deliveries for others. - Low: issues needing the master key or operator access; DoS that requires unauthenticated volume (there is no inbound rate limiting by design; it is documented in `AGENTS.md` under Known Gaps). ## Out of scope / leave alone - No inbound rate limiting on `/v1` (documented; operators front it with a proxy). - Endpoints being open when `SPARROW_API_KEY` is unset. - Deliveries to private addresses when the operator explicitly allowed them. - `verify_ssl: false` disabling TLS verification for that webhook (documented). - Satellites under `satellites/` (the CLI, recipes, `sparrow-sources`, `sparrow-sinks`). They are helper tools outside the core: they reach Sparrow only through the public API, with the credential the operator gives them, so nothing they do bypasses a boundary above. Report bugs in them, including signature or replay handling in sources and sinks, as ordinary issues. - `cmd/benchmark`, `scripts/`, `e2e/`, `docs/`, `book/`, generated clients in `client/`, and `*_otel.go` generated wrappers (report the generator instead). ## Reports Include the attacker's starting credential (none, consumer token, tenant token, master key), the request sequence, and a reproducer that runs against a local PostgreSQL (`make dev-db`), ideally as a Go test. Proposed patches should follow `AGENTS.md` conventions and come with a regression test.