CapacityLens threat model
Baseline version: 2026-09-23. Authentication posture update: 2026-10-09. The update records the removal of local MFA below against the 2026-07-14 security review, with its 2026-10-01 addendum; it is not a fresh review of the full model. Review this model after changes to authentication, tenancy, imports, offline storage, deployment topology or external services.
Security objectives
- A user can read or change only accounts, operations, records and protected fields allowed by their current server-side membership and role.
- Passwords, session tokens, reset/invite tokens, historical MFA seeds/recovery codes and provider secrets are not disclosed or stored in recoverable form where a safer representation is possible.
- Tenant writes remain valid, atomic and attributable; corrupted relational state prevents startup.
- Browser-delivered code cannot silently turn an authenticated browser into a cross-site write primitive, and sensitive API responses are not cached.
- Operators can contain a compromised identity, restore authoritative data and investigate typed security/audit events without relying on the compromised host alone.
CapacityLens is not a safety-critical, payments, classified-data, real-time media or anonymous public-upload system. Availability is important but confidentiality and tenant integrity take priority over keeping a misconfigured production process running.
Assets and trust boundaries
| Asset | Primary protection | Boundary |
|---|---|---|
| Account schedule and private client/project names | Server membership, action/field authorization, SQLite constraints | Browser/API and tenant boundary |
| Identity, password and provider-link state | Better Auth, versioned scrypt, encrypted provider tokens, explicit verified linking | Auth/provider/API and database boundary |
| Session, reset and invite bearer values | HttpOnly cookies or one-time values; hashes where supported; expiry/revocation | Browser/API and operator delivery boundary |
| Offline snapshot | Opt-in, role-filtered, AES-256-GCM, seven-day expiry, viewer-only | Browser-origin/device boundary |
| Database, WAL, audit and snapshots | 0600 files, 0700 backup directory, operator-provided encrypted volumes | Process/host boundary |
| Audit and security events | Data-minimised JSON, local restrictive file plus optional separately forwarded stream | Process/log-collector boundary |
| Build and release inputs | Lockfile, pinned images/actions, dependency review, SBOM, scans and provenance | Contributor/CI/registry boundary |
The packaged production flow is browser → public TLS proxy → unprivileged web container → verified per-install internal TLS → unprivileged HTTPS API container → local SQLite/backup volumes. A bare-metal deployment may instead use HTTP only over a same-host loopback hop. In both shapes the API must not be publicly reachable and the proxy must overwrite rather than append forwarding headers.
Actors
- Viewer, editor, admin and owner, all potentially malicious within their legitimate account.
- An authenticated user attempting cross-account or higher-role access.
- An unauthenticated internet attacker, automated credential attacker or cross-site origin.
- A compromised browser profile or device.
- A malicious/compromised identity provider, dependency, build input or container base.
- A self-hosting operator who makes an accidental or unsafe configuration choice.
- A host operator misusing stopped-server SSO repair or sole-Owner recovery authority.
- A host-level attacker. Host compromise is not fully preventable in-process; encrypted storage, secret management, isolation and off-host logs/backups limit consequences.
Abuse cases and controls
| Threat | Principal controls | Verification |
|---|---|---|
| BOLA/IDOR or cross-tenant mutation | Membership fetched server-side for each operation; every scoped entity has accountId; row-addressed generic writes make absent and foreign ids response-indistinguishable; row/reference validation fails closed when a project-bound allocation cannot resolve its project in the same account; cross-account tests | app.authz, app.members, tenant-store, route and shared mutation tests |
| Function/field privilege escalation | Central action matrix; protected-name projection/preservation; owner-only import; fresh session for privileged actions | access, privacy and route tests |
| Credential stuffing/password cracking | Positive global/API throttling; 15–128 Unicode code points; HIBP range check; scrypt N=2^17,r=8,p=1; no default password | password/auth/rate-limit tests |
| Password-only account takeover | Long passwords, HIBP by default, scrypt, throttling and bounded/revocable sessions; there is no local MFA for password sign-in | real auth integration and UI tests |
| Session theft/fixation | Secure HttpOnly SameSite __Host- cookies; new token on auth; fixed 12-hour and 30-minute idle limits; revocation/reset invalidation; session inventory | auth and member revocation tests |
| CSRF and cross-origin data use | Unsafe-method Origin/Sec-Fetch-Site rejection; exact configured or trusted-proxy-derived same origin; SameSite cookie; safe HTTP methods | CSRF/CORS and packaged-proxy tests |
| Injection/XSS/mass assignment | React text rendering; no untrusted HTML; parameterized SQLite; explicit table/column codecs; sanitisation and structural limits; CSP | server/shared/CSP tests |
| Malicious, stale or oversized import | JSON-only, 5 MiB/200,000-record caps; Owner-only access; bounded/cancellable worker preparation; schema migration/sanitisation; tenant remap; reference validation; exact-snapshot recheck and atomic replacement | import, worker, transaction and mutation tests |
| Offline cache disclosure/tampering | Role-filtered input; non-extractable device key; AES-GCM with random IV/AAD; tamper/expiry deletion; viewer-only | offline cache tests |
| Database corruption/partial write | Startup foreign-key check; WAL; transactions; optimistic concurrency; sync-session ordering/provenance; atomic imports/backups | migration, ordering, transaction and restore-drill tests |
| Log erasure/injection or invisible attack | Structured serialization, no values/credentials, restrictive modes, health degradation latch and optional separate JSON forwarding | audit/log/production-guard tests |
| Provider substitution or invalid claims | Google/Microsoft provider-specific OAuth flow; Microsoft tenant, issuer, audience, expiry, identity and nonce checks; no email-based account merging | provider callback and account-admission tests |
| Federated identity takeover | Verified matching email for explicit links; implicit linking disabled; provider/subject uniqueness; invitation/bootstrap admission; provider-required access checks | identity-port, onboarding and provider integration tests |
| Operator recovery misuse | Stopped server and exclusive SQLite lock; unique sole-Owner eligibility; ordinary expiring single-use reset; rollback on partial failure; token-free audit record | Owner-recovery CLI and audit tests |
| Resource exhaustion | 512 accepted-socket ceiling; per-IP application/CSP rate limit with constant-work health exempt; bounded scrypt, HIBP and import queues; 5,000-operation batch and 200,000-record import caps; request/queue/provider timeouts | resource-queue/rate-limit/health/import/CSP tests |
| Supply-chain compromise | Exact lockfile, pinned action/base-image commits/digests, Dependabot, CodeQL, Gitleaks, dependency review, SBOM, Trivy, ZAP and tagged provenance | local gates and public/manual workflows |
Residual and accepted risks
- Google and Microsoft use Better Auth's provider OAuth flow for state, PKCE and cookies. CapacityLens checks provider-specific claims and account admission before creating a local identity. GitHub remains experimental in mixed mode and cannot meet provider-required sign-in. Provider configuration still needs installation-specific interoperability, MFA-policy and logout/session-lifetime testing.
- IdP disablement does not revoke already-issued local sessions. The accepted maximum is the remaining twelve-hour absolute lifetime or thirty minutes inactivity; local revocation is an incident-response requirement and back-channel logout must be reconsidered before hosted GA.
- Identity masquerade is a session-scoped, account-confined read projection for Owners and Admins. A root unsafe-method guard and a separate Better Auth proxy guard reject changes while it is active; actor-dependent reads use the target member's role and redaction. The process-local registry deliberately disappears on restart, which ends access rather than restoring uncertain state. Start and end events are audited, with the start event carrying the session expiry that bounds a record if the process stops before an end event can be written.
- SSO-only mode leaves local password credentials available if the operator restores password mode. Protecting the host/database and using the stopped-server recovery procedures carefully remain operator responsibilities.
- The sole-Owner recovery command is deliberate host-operator authority. It cannot be contained from an attacker who already controls the application database and process environment.
- Password-only deployments have no local MFA and do not meet ASVS 5.0 Level 2 requirement V6.3.3. If an agency requires MFA for Google or Microsoft company login, the operator configures it through the provider's policy; CapacityLens cannot enforce or verify that policy. A federated session does not establish whether the provider challenged with MFA.
- Existing legacy Better Auth scrypt hashes use the former weaker profile until the user changes or resets the password. They are verify-only; new material never uses that format.
- The application has no IP/device-risk engine, anomalous-login user notification, global administrator “revoke everyone” control or HSM/full-memory encryption.
- Public TLS, encrypted host volumes, secret-manager/HSM use, clock synchronization, log retention and off-host collection are deployment controls. Internal service TLS is application-packaged for Compose and optional for a same-host bare-metal hop; startup warnings cannot verify external controls.
- An unlocked or compromised application origin can invoke its non-extractable offline key. Device encryption, patching and profile access control remain necessary.
- A single-process SQLite service can still be denied service by sufficient network or tenant-valid load. Edge rate limiting, connection limits and resource monitoring remain operator controls.