diff --git a/.oss-scanner/Dockerfile b/.oss-scanner/Dockerfile new file mode 100644 index 00000000..52cf146e --- /dev/null +++ b/.oss-scanner/Dockerfile @@ -0,0 +1,36 @@ +# Image for Anthropic's OSS Scanner: it fetches every dependency while the network is available and builds the binary as docker/Dockerfile does +# Everything after this build runs offline, see threat_model.md next to this file + +# Go version must match backend/go.mod +FROM golang:1.27.3-trixie AS go-toolchain + +# Node version must match .node-version +FROM node:24-trixie +COPY --from=go-toolchain /usr/local/go /usr/local/go +ENV PATH="/root/go/bin:/usr/local/go/bin:${PATH}" \ + CI=true + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git build-essential sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +COPY . /src +WORKDIR /src + +# Fetch the JS workspace and Go modules, the only steps that need the network +RUN npm install --global "$(node -p "require('./package.json').packageManager.split('+')[0]")" \ + && pnpm install --frozen-lockfile \ + && cd backend && go mod download + +# Build the SPA into backend/frontend/dist so the binary embeds it like a release build +# The e2etest build adds the seed and token-forging routes from the end-to-end suite, see threat_model.md +RUN pnpm build \ + && cd backend/cmd && go build -o /src/backend/pocket-id . \ + && go build -tags e2etest -o /src/backend/pocket-id-e2etest . + +# Pin the Go toolchain to what is installed so nothing below tries to download modules or toolchains +ENV GOPROXY=off \ + GOSUMDB=off \ + GOTOOLCHAIN=local + +# Run the unit tests to warm the build cache and prove the image works offline; Postgres tests skip without POCKET_ID_TEST_POSTGRES_URL +RUN cd backend && (timeout 1200 go test -tags exclude_frontend,unit ./... || echo "WARNING: go test reported failures") diff --git a/.oss-scanner/threat_model.md b/.oss-scanner/threat_model.md new file mode 100644 index 00000000..511b4f4a --- /dev/null +++ b/.oss-scanner/threat_model.md @@ -0,0 +1,115 @@ +# Pocket ID threat model + +This file guides Anthropic's OSS Scanner. Confirm paths and setting names against the scanned checkout; the code moves faster than this document. + +## 1. What Pocket ID is + +Pocket ID is a self-hosted OpenID Connect provider where users sign in with passkeys only, so there are no passwords to steal. +Operators put it in front of their other services (Nextcloud, Immich, Proxmox, reverse-proxy auth and so on), which makes a compromise of Pocket ID a compromise of everything that trusts it. + +- `backend/` is a Go server (gin, GORM, SQLite or Postgres) that serves the API, the OIDC endpoints and the embedded SPA on one port (default 1411). +- OAuth and OIDC come from a fork of ory/fosite (`github.com/pocket-id/fosite`), composed in `backend/internal/oidc/provider.go`. +- `frontend/` is a SvelteKit SPA, built into `backend/frontend/dist` and embedded with `go:embed`. +- Background work (cron jobs, rate limiters, one-time codes, signup tokens, LDAP and SCIM sync) runs as compiled-in Go actors on the Francis actor runtime, stored in the same database. + +## 2. Principals and trust boundaries + +From least to most trusted: + +1. **Anonymous network attacker**: reaches every `.Public()` and `.Optional()` route, the OIDC endpoints and the SPA. +2. **OIDC client**: a relying party with a client ID and, if confidential, a client secret or a federated client assertion; it may be malicious or compromised. +3. **Signed-in user**: holds a session cookie and the `account:*` scopes; may be restricted to `unverifiedScopes` while email verification is pending. +4. **API key**: sent as `X-API-Key`, inherits its owner's scopes except the session-only ones (`account:session`, `account:authorize`, `account:passkeys:enroll`, `account:api-keys:create`). +5. **Admin**: a user with `IsAdmin`, who manages users, groups, OIDC clients, API resources and the application configuration. +6. **Operator**: controls environment variables, the database, the storage backend and the `pocket-id` CLI (`import`, `export`, `key-rotate`, `encryption-key-rotate`, `one-time-access-token`). + +Authorization lives in `backend/internal/authz/` (`scope.go`, `principal.go`, `middleware.go`, `router.go`). +Every route declares a scope or is explicitly public, and the user is reloaded on every request so disabling a user or removing admin takes effect at once. +OAuth access tokens issued to relying parties cannot call the management `/api`. + +## 3. Attack surface + +### OAuth 2.0 and OIDC (`backend/internal/oidc/`) + +- `/authorize`, `/api/oidc/par`, `/api/oidc/token`, `/api/oidc/userinfo`, `/api/oidc/introspect`, `/api/oidc/device/*`, `/api/oidc/end-session`, the interaction endpoints and the `/.well-known/*` documents. +- Grants: authorization code with PKCE, refresh token (with reuse detection), device code and client credentials. +- Redirect URI matching, including wildcards, lives in `backend/internal/utils/callback_url_util.go`. +- Client ID Metadata Documents (`oidc/cimd.go`) and federated client authentication (`oidc/federated_client_auth.go`) fetch remote documents and JWKS. +- Back-channel logout (`backend/internal/backchannellogout/`) sends signed logout tokens to relying parties. +- Group restrictions on clients, custom claims and API resources and permissions (`backend/internal/api/`, `oidc/api_resource.go`) decide what a user may authorize and what ends up in tokens. + +### Sign-in and account recovery + +- Passkeys (`backend/internal/webauthn/`): login, registration, re-authentication for sensitive clients; the RP ID and origin come from `APP_URL`. +- One-time access codes and email login codes (`backend/internal/onetimeaccess/`), including the unauthenticated email request when an admin enables it. +- Cross-device login (`backend/internal/devicelogin/`). +- Self-signup and signup tokens (`backend/internal/usersignup/`), and the initial admin setup at `/api/signup/setup`. +- Email verification and email change (`backend/internal/emailverification/`). +- Sessions are HS256 JWTs in the `__Host-access_token` cookie (`service/jwt_service.go`, `utils/cookie/`). + +### Management API + +- Users, groups, OIDC clients, API keys (`backend/internal/apikey/`), application configuration (`backend/internal/appconfig/`) and audit logs (`backend/internal/auditlogs/`). +- `STATIC_API_KEY` authenticates as a synthetic admin when an operator sets it. + +### Uploads and rendered content + +- Application images, client logos and profile pictures (`controller/app_images_controller.go`, `controller/oidc_controller.go`, `controller/user_controller.go`) accept PNG, JPEG, ICO and SVG and are served from the same origin. +- Storage backends are the filesystem (`os.Root`), S3 or the database (`backend/internal/storage/`). +- The SPA renders user-controlled names, client names and URLs, and handles `redirect` parameters on the login pages. + +### Outbound requests (`backend/internal/outbound/`) + +- Client logo URLs, CIMD documents, federated JWKS, SCIM provisioning (`backend/internal/scimsync/`), back-channel logout and LDAP profile pictures go through a guarded transport that checks each dialed IP and every redirect. +- The `OUTBOUND_ALLOWED_HOSTS_*` variables let operators allow more destinations per purpose. + +### LDAP sync (`backend/internal/ldapsync/`) + +- Users and groups, including admin status, are imported from an admin-configured directory, so directory data is semi-trusted input. + +### Web hardening + +- CSRF protection through `http.CrossOriginProtection` with `APP_URL` as the trusted origin, a nonce-based CSP (`utils/csp.go`), CORS only on the endpoints relying parties call from browsers (`middleware/cors.go`) and per-IP rate limits (`middleware/rate_limit.go`). +- Client IPs come from the socket unless `TRUST_PROXY`, `TRUSTED_PLATFORM` or `PROXY_PROTOCOL` say otherwise. + +## 4. Secrets and data worth protecting + +- `ENCRYPTION_KEY` is the root secret: it encrypts the signing keys and the session key at rest, derives the fosite HMAC secret for codes and refresh tokens, encrypts SCIM bearer tokens and keys the actor runtime's mTLS. +- The signing key (RS256 by default) signs ID tokens and JWT access tokens; the session key signs Pocket ID's own sessions. +- Client secrets and API keys are stored hashed, refresh tokens and codes as fosite HMAC signatures. +- Passkey credentials, user profiles, group memberships, audit logs and SMTP and LDAP credentials in the application configuration. + +## 5. How to exercise it + +The image has the checkout at `/src`, Go modules and the pnpm store pre-fetched and `GOPROXY=off`; there is no network. + +- `/src/backend/pocket-id` is the release build with the SPA embedded. +- `/src/backend/pocket-id-e2etest` adds the end-to-end routes: `POST /api/test/reset?skip-ldap=true` seeds users (`tim` is an admin, `craig` and `eddy` are not), groups and OIDC clients, and `POST /api/test/accesstoken` signs tokens for them; see `tests/data.ts` for the seed data. +- Start a server with `cd /tmp && ENCRYPTION_KEY=test-encryption-key APP_URL=http://localhost:1411 APP_ENV=test /src/backend/pocket-id-e2etest`; data goes to `./data` with SQLite, and the seed's signing key is encrypted with exactly that `ENCRYPTION_KEY`. +- Passkeys cannot be used without a browser and authenticator, so sign in as a user with `pocket-id one-time-access-token ` (same env vars) and exchange the code with `POST /api/one-time-access-token/`, which sets the session cookie. +- Unit tests: `cd /src/backend && go test -tags exclude_frontend,unit ./...`; tests that need Postgres skip without `POCKET_ID_TEST_POSTGRES_URL`, and no Postgres is installed. +- Regression tests belong next to the code they test, as `*_test.go` with the `unit` build tag where test helpers are needed; `backend/internal/utils/testing/` has database and actor helpers. +- The Playwright suite in `tests/` needs Docker and a browser and cannot run here. + +## 6. Report requirements + +- State the commit, the lowest principal from section 2 that can trigger the issue and any non-default setting it needs. +- Give a reproducer against a running server (an HTTP request sequence) or a failing Go test, and a patch with a regression test where possible. +- Trace the route and its scope declaration in `authz` before claiming a missing check, and say whether the result was demonstrated or only read from source. +- One report per root cause; report dependency issues only with a path reachable through Pocket ID. + +## 7. Severity + +- **Critical**: an anonymous attacker or a malicious OIDC client signs in as another user, obtains tokens for another user, becomes admin, forges tokens or reads `ENCRYPTION_KEY`, signing keys or the session key; remote code execution. +- **High**: a regular user gains admin or another user's account; a client receives claims, scopes or groups the user did not grant or is not allowed; redirect URI validation lets codes or tokens reach an attacker; stored XSS that runs in an admin's session; SSRF through the guarded transport to an address the policy blocks with default settings; a bypass of a passkey, user verification or re-authentication requirement. +- **Medium**: account or email enumeration, bypassing rate limits on code entry, CSRF on a state-changing request, information disclosure of other users' profile data, refresh-token reuse detection bypass, denial of service by an unauthenticated attacker with modest traffic. +- **Low**: issues that need an admin or a non-default insecure setting, self-XSS, missing hardening without a demonstrated impact, timing differences without a practical attack. + +## 8. Out of scope + +- Admins and operators are trusted: managing users, clients and configuration, uploading images, configuring LDAP, SCIM or SMTP and running the CLI are not vulnerabilities unless a less privileged principal gains those powers. +- Whoever completes `/api/signup/setup` on a fresh instance becomes the first admin by design; races or bypasses after an admin exists are in scope. +- The `e2etest` build tag and its unauthenticated routes exist only for the test suite and are never in release builds. +- Settings that knowingly weaken security (`TRUST_PROXY=true`, `DISABLE_RATE_LIMITING`, `*_SKIP_CERT_VERIFY`, `OUTBOUND_ALLOWED_HOSTS_*`, `STATIC_API_KEY`, non-production `APP_ENV`) cap a finding at low unless the default configuration is affected. +- Generated code (`frontend/src/lib/paraglide/`, `backend/frontend/dist/`, `backend/resources/email-templates/`), the Playwright suite in `tests/`, CI and developer tooling. +- Volumetric denial of service and missing security headers on static assets.