Files
TX/threat_model.md
T
riyadhafraa 7a2ae8434d Update project documentation and code comments to remove platform-specific references
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu
Replit-Helium-Checkpoint-Created: true
2026-05-14 06:23:49 +00:00

16 KiB
Raw Blame History

Threat Model — Tx OS

Version: 1.0 Companion document: SECURITY_REVIEW.md (manual review findings, kept internally).

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 self-hosted container stack (Express API + Postgres + S3-compatible object storage such as MinIO) 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   │ ─────────▶ │  TLS-terminating reverse proxy  │    │
│   │  (SPA)     │            │  (Caddy / Nginx / Traefik)      │    │
│   └────────────┘            └──────────────┬──────────────────┘    │
│                                            │                       │
└────────────────────────────────────────────┼───────────────────────┘
                                             │ HTTP (proxied)
                                             ▼
                              ┌──────────────────────────────┐
                              │  Express API server          │
                              │  (artifacts/api-server)      │
                              │  • express-session (Postgres)│
                              │  • Socket.IO                 │
                              │  • Drizzle ORM               │
                              └────────────┬─────────────────┘
                                           │
            ┌──────────────────────────────┼──────────────────────────────┐
            │                              │                              │
            ▼                              ▼                              ▼
    ┌───────────────┐            ┌──────────────────┐            ┌───────────────┐
    │ PostgreSQL    │            │ MinIO / S3       │            │ Local FS      │
    │ (container)   │            │ (signed URLs,    │            │ (dev fallback │
    │               │            │  private network)│            │  driver only) │
    └───────────────┘            └──────────────────┘            └───────────────┘

Trust boundaries (numbered):

  1. Browser ↔ Edge proxy. TLS-terminated by an operator-controlled reverse proxy (Caddy / Nginx / Traefik). The SPA is the only first-party client.
  2. Edge ↔ API server. Loopback or private docker network. The API server treats X-Forwarded-For as trusted (app.set("trust proxy", 1)); the operator MUST ensure only the edge proxy can reach the API port.
  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. In production the API server signs S3 PUT URLs against a self-hosted MinIO endpoint over the private docker network; in local development without S3 the API signs HMAC tokens validated by its own /api/storage/_local/upload route, persisting to the local filesystem under ./storage/. 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. 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. Cross-service traffic between the API container and MinIO / Postgres rides the private docker network only; no service should be exposed on the public interface other than the nginx web container (which is itself fronted by an operator-managed TLS-terminating reverse proxy).

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 the internal security review.

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 endpoint S3_ENDPOINT env is operator-controlled and resolved over the private docker network only; signed-URL responses include the bucket/key the API issued 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 Built-in route 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 Both fields locked together 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 the operator deploys the docker-compose stack (private VPS or on-prem). The operator chooses the region and is responsible for any regulatory 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 the internal security review (per-finding) and the dependency-scan report (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 — production-runtime patches for fast-xml-builder, lodash, path-to-regexp, postcss.
  7. Dependency Batches BC — build-tool and codegen patches.
  8. MR-L1 / MR-L2 / MR-L3 / MR-L6 / MR-L7 / MR-L8 — defense-in-depth and hardening.