Add threat model to document system security guarantees

Adds a threat model document detailing assets, trust boundaries, and security guarantees for the system.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ebda9586-5b27-42a2-8013-0d6a0e3b9966
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
riyadhafraa
2026-05-13 08:03:09 +00:00
parent 19705793d0
commit 858e9d4b87
+183
View File
@@ -0,0 +1,183 @@
# Threat Model — Tx OS
**Date:** 2026-05-13
**Version:** 1.0 (initial)
**Companion documents:** `.local/security/scan-report.md` (automated scanners), `.local/security/manual-review.md` (manual review findings).
> This document captures Tx OS's assets, trust boundaries, threats (STRIDE-style), and the security guarantees the system commits to upholding. It is the design-level companion to the per-finding manual review and is intended to outlive any individual fix task.
---
## 1. System overview
Tx OS is an internal "office OS" for a single-tenant organization. It is deployed as a Replit app (single Express + Postgres + Object Storage stack) with a React/Vite SPA, a small mobile-friendly UI, and a real-time notification layer over Socket.IO. The app is bilingual (Arabic / English), and the workforce uses it to:
- Order internal services (e.g. coffee, printing).
- Manage notes, including shared-folder collaboration with checklist items.
- Schedule and track Executive Meetings, including PDF exports and an alert popup.
- Administer apps, roles, permissions, groups, and user accounts.
The app is **single-tenant** — there is no multi-tenant isolation requirement; all authenticated users belong to the same organization. The core authorization model is per-user roles + per-user groups, with permissions attached to roles and visibility attached to apps.
---
## 2. Assets
| Asset | Sensitivity | Where it lives | Why it matters |
|---|---|---|---|
| User credentials (`users.password_hash`) | Critical | Postgres `users` table | Compromise = account takeover for every user. |
| Session ids | Critical | Postgres `user_sessions` (connect-pg-simple) + browser cookie | Session hijack = full impersonation. |
| Password reset tokens (hashed) | High | `password_reset_tokens` table | One-shot account takeover if intercepted. |
| Executive meeting content (titles, attendees, notes, location, URL) | High | `executive_meetings*` tables | Org-confidential. Attendee lists reveal org structure. |
| Generated PDF archives | High | Object Storage `PRIVATE_OBJECT_DIR/uploads/<uuid>` | Same content as above, persisted. |
| Notes content (titles, body, checklist, replies) | High | `notes*` tables | User-private; some shared via folders. |
| User directory (name, email, avatar) | Medium | `users` table | Phishing fuel. |
| Service order history | Medium | `service_orders` table | Personal consumption record. |
| Audit logs (`audit_logs`, role/permission audit) | Medium | Postgres | Tamper-evidence; integrity matters more than confidentiality. |
| App / role / permission configuration | Medium | `apps`, `roles`, `permissions`, `app_permissions` | Mis-edit = privilege change. |
| Brand logo + uploaded service images / avatars | Low | Object Storage | Public-ish; logo is rendered into PDFs. |
---
## 3. Trust boundaries
```
┌─────────────────────────────────────────────────────────────────────┐
│ Public internet │
│ │
│ ┌────────────┐ HTTPS ┌──────────────────────────────────┐ │
│ │ Browser │ ─────────▶ │ Replit edge (mTLS-terminated │ │
│ │ (SPA) │ │ proxy, deploys.replit.app) │ │
│ └────────────┘ └──────────────┬──────────────────┘ │
│ │ │
└────────────────────────────────────────────┼───────────────────────┘
│ HTTP (proxied)
┌──────────────────────────────┐
│ Express API server │
│ (artifacts/api-server) │
│ • express-session (Postgres)│
│ • Socket.IO │
│ • Drizzle ORM │
└────────────┬─────────────────┘
┌──────────────────────────────┼──────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌───────────────┐
│ PostgreSQL │ │ Replit Object │ │ Replit sidecar│
│ (Replit- │ │ Storage (GCS via │ │ http://127. │
│ managed) │ │ signed URLs) │ │ 0.0.1:1106 │
└───────────────┘ └──────────────────┘ └───────────────┘
```
**Trust boundaries (numbered):**
1. **Browser ↔ Replit edge.** TLS-terminated by the platform proxy. The SPA is the only first-party client.
2. **Edge ↔ API server.** Loopback within the Replit container. Trust is established via the upstream proxy's mTLS. The API server treats `X-Forwarded-For` as trusted (`app.set("trust proxy", 1)`).
3. **API server ↔ Postgres.** Connection over the in-cluster network; credentials in `DATABASE_URL`. Drizzle ORM is the only query path.
4. **API server ↔ Object Storage.** Signed-URL pattern via the Replit sidecar at `http://127.0.0.1:1106` (loopback HTTP — see Task #522 SAST suppression). All upload URLs are short-lived (15 min) and target `randomUUID()` paths under `PRIVATE_OBJECT_DIR`.
5. **API server ↔ Socket.IO clients.** Same origin as the API server. Auth shared via session cookie; every socket joins exactly its `user:${userId}` room.
---
## 4. Security guarantees the system must uphold
These are the invariants every change must preserve. A regression on any one of them is, by default, at least High severity.
- **G1 — Authentication required.** Every `/api/*` endpoint other than the intentionally-public set requires a valid session. The intentionally-public set today is: `/api/auth/login`, `/api/auth/register`, `/api/auth/forgot-password`, `/api/auth/reset-password`, `/api/auth/reset-password/verify`, `/api/health`, `/api/settings` (see MR-L5 — recommended to remove from this list), and `/api/storage/public-objects/*`. Adding any new endpoint to this list must be a deliberate, reviewed change.
- **G2 — Server-side authorization.** UI hiding is *not* a permission boundary. Every mutating endpoint and every read of non-public data must enforce its permission server-side, even if the SPA hides the control.
- **G3 — Cross-user isolation.** A user can never read or modify another user's notes, orders, password, settings, sessions, or notifications unless an explicit share / group / admin permission grants it.
- **G4 — Built-in app routes are immutable** (Task #517). Slugs in `BUILTIN_APP_SLUGS` cannot have their `route` or `slug` mutated, even by an admin, because the SPA hardcodes those routes.
- **G5 — Sensitive HTML is sanitized.** Every user-supplied string that enters an HTML or PDF rendering path passes through `htmlToSafeHtml`/`safeHtml` (`sanitize-html` + DOMPurify) with the established allow-list.
- **G6 — Secrets never leave the server.** No `.env` value is sent to the browser. `SESSION_SECRET` is required in production (`app.ts:64`).
- **G7 — Database access via parameterized queries only.** Drizzle ORM template tags and the column-name allow-list in `executive-meetings.ts:109` are the only `sql\`\`` consumers; no string-spliced SQL is permitted.
- **G8 — Loopback-only services stay loopback.** The Object Storage sidecar at `http://127.0.0.1:1106` is the only intentional plaintext fetch and must never be reachable from outside the container.
---
## 5. STRIDE threats
For each STRIDE category, the table lists the most consequential threats, the relevant assets, current mitigations, and any open gaps. References like *(MR-H1)* point to entries in `.local/security/manual-review.md`.
### 5.1 Spoofing
| # | Threat | Mitigation today | Gap |
|---|---|---|---|
| S1 | Login as another user via stolen password | Bcrypt password hashing; session cookie `httpOnly` | No rate limit on `/auth/login` *(MR-H3)*; bcrypt cost 10 instead of 12 *(MR-L2)*; account enumeration on register *(MR-L3)* |
| S2 | Hijack victim's session by fixing a session id | `connect.sid`, `httpOnly`, `secure` (prod), `sameSite=lax`, `trust proxy=1` | No `req.session.regenerate` after login *(MR-M2)* |
| S3 | Spoof another user over the realtime channel | Server-side `userId` from session; per-user rooms | None — the server defines no client-originated socket events |
| S4 | Spoof the Object Storage sidecar | Hardcoded loopback URL, no DNS lookup | None |
### 5.2 Tampering
| # | Threat | Mitigation today | Gap |
|---|---|---|---|
| T1 | Cross-site request forces state change as the victim | `sameSite=lax` cookie, custom `Content-Type: application/json` for most writes | No CSRF token; `lax` permits top-level GET/POST navigation *(MR-M1)* |
| T2 | Mutate another user's note/order/meeting | Per-route permission checks; tenant-scoping via `userId`/`folderId` lookups | OK — no mass-assignment issues found |
| T3 | Edit a built-in app's route to break navigation | Task #517 lock with regression tests | OK — `apps-builtin-route-lock.test.mjs` covers it |
| T4 | Pollute the audit log with fake events | `app.opens` audit row is recorded for any caller | `POST /apps/:id/open` doesn't gate on visibility *(MR-M5)* |
| T5 | Pollute the PDF archive list with attacker-chosen `filePath` | `requireExecutiveAccess` only | Any executive role can write arbitrary paths *(MR-H2)* |
### 5.3 Repudiation
| # | Threat | Mitigation today | Gap |
|---|---|---|---|
| R1 | Admin denies having changed a permission | `permission-audit.ts` records role/permission changes | OK |
| R2 | User denies sending a note | `notes` table has `createdAt` + sender; reply chain timestamped | OK |
| R3 | App-open audit is unreliable due to T4 above | — | Same fix as T4 *(MR-M5)* |
### 5.4 Information disclosure
| # | Threat | Mitigation today | Gap |
|---|---|---|---|
| I1 | Read another user's PDF or avatar by guessing object UUID | Object paths are random UUIDs | `/api/storage/objects/*` only requires auth, not per-object access *(MR-H1)* |
| I2 | Enumerate executive meeting dates as an unprivileged user | Per-meeting fetches require `executive_access` | The realtime `executive_meetings_changed` and `executive_meeting_notifications_changed` events are global broadcasts *(MR-L1)* |
| I3 | Harvest the org's user directory | `requireAuth` gate | No permission gate; entire directory is visible to every user *(MR-M4)* |
| I4 | Leak DB schema via verbose error messages | Most routes return coded short strings | `executive-meetings.ts` sites echo `Error.message` *(MR-M6)* |
| I5 | Leak SPA bundle internals via missing CSP | TLS only | No `helmet` / CSP *(MR-M3)* |
| I6 | Leak public flags via `/api/settings` | — | Endpoint is unauthenticated *(MR-L5)* |
### 5.5 Denial of service
| # | Threat | Mitigation today | Gap |
|---|---|---|---|
| D1 | Brute-force `/auth/login` until success | bcrypt slowdown only | No rate limit *(MR-H3)* |
| D2 | Flood `/auth/forgot-password` to fill table + send mail | None | No rate limit *(MR-H3)* |
| D3 | Open thousands of sockets to exhaust file descriptors | `sessionMiddleware` gate at handshake | No per-user / per-IP cap *(MR-L4)* |
| D4 | Send giant JSON bodies to slow parse | `express.json()` default 100 KB | No tighter cap *(MR-L7)* |
| D5 | Trigger expensive PDF render in a loop | `requireExecutiveAccess` only | No per-user PDF render rate limit (latent) |
### 5.6 Elevation of privilege
| # | Threat | Mitigation today | Gap |
|---|---|---|---|
| E1 | Low-priv user reads admin-only meeting PDFs | — | *(MR-H1, MR-H2)* — currently exploitable via storage download |
| E2 | Low-priv user changes their own role | `roles.ts` is `requireAdmin` | OK |
| E3 | User edits another user's password | `users.ts` password change is `requireAdmin` or self-only | OK |
| E4 | Bypass built-in route lock by renaming slug then route | Task #517 locks both fields with regression tests | OK |
| E5 | Smuggle XSS into PDF or note rendering | `htmlToSafeHtml` + DOMPurify allow-list | OK at HEAD, but no CSP backstop *(MR-M3)* |
---
## 6. What is explicitly out of scope
- **Multi-tenant isolation.** Tx OS is single-tenant. There is no per-tenant data partition.
- **Data residency.** Production data is stored wherever Replit hosts the project; no regional commitments.
- **Penetration testing of the live deployment.** This document is design-level only.
- **Compliance frameworks** (SOC 2 / ISO 27001). Not in scope today; the threat model is structured so the most material gaps would map cleanly into a future compliance effort if one is opened.
---
## 7. Open recommendations (prioritized for follow-up)
The detailed remediation text lives in `.local/security/manual-review.md` (per-finding) and in `.local/security/scan-report.md` (dependency batches). The order below is the recommended fix sequence based on risk × effort.
1. **MR-H1 + MR-H2 + MR-M7** — wire up object-level authorization for `/api/storage/objects/*` and constrain the PDF archive write endpoint. *(Highest user impact; one focused change to `routes/storage.ts` plus a small change to `pdf-archives` write.)*
2. **MR-H3 + MR-L4** — add `express-rate-limit` on auth endpoints and a per-user socket cap. *(Small library change, big DoS / brute-force win.)*
3. **MR-M1 + MR-M2** — CSRF posture (move session cookie to `sameSite=strict` if no third-party deep-links, otherwise add a CSRF token middleware) + `req.session.regenerate` on login.
4. **MR-M3** — add `helmet()` and a basic CSP for the SPA bundle.
5. **MR-M4 + MR-M5 + MR-M6 + MR-L5** — tighten the small set of over-exposed read endpoints (directory, settings) and audit-log write paths.
6. **Dependency Batch A** (from Task #522) — production-runtime patches for `fast-xml-builder`, `lodash`, `path-to-regexp`, `postcss`.
7. **Dependency Batches BC** (from Task #522) — build-tool and codegen patches.
8. **MR-L1 / MR-L2 / MR-L3 / MR-L6 / MR-L7 / MR-L8** — defense-in-depth and hardening.