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:
+183
@@ -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 B–C** (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.
|
||||
Reference in New Issue
Block a user