Files
TX/threat_model.md
T
riyadhafraa 143ad9a29d Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project
can be cloned and run on any Linux VPS with `docker compose up`.

Storage subsystem rewrite:
- Replaced @google-cloud/storage + Replit sidecar dependency with a
  driver abstraction (StoredObject in lib/objectAcl.ts) and two
  implementations: LocalDriver (filesystem + HMAC-signed PUT route at
  /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint
  via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected
  by STORAGE_DRIVER / S3_ENDPOINT env vars.
- Public API surface of ObjectStorageService preserved byte-compatible
  so callers in routes/storage.ts and routes/executive-meetings.ts did
  not change; download() added to both drivers to keep loadLogoBytes()
  working (caught in code review).
- Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in
  test C) all pass against the new local driver. Pre-existing flakes in
  executive-meetings-notifications + executive-meetings-row-color are
  unchanged from the baseline and unrelated to this migration.

Infrastructure:
- Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses
  the official Playwright base image so PDF rendering works in-container;
  web stage is nginx serving the Vite SPA bundle.
- docker-compose.yml: postgres + minio + minio-init (creates buckets) +
  api + web + one-shot migrate runner. Healthchecks on every long-lived
  service.
- docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket
  upgrade ordering.
- .env.example: every runtime env var documented with comments.
- README.md replaces replit.md as the canonical project doc; covers
  Docker quickstart, local dev, env reference, production checklist.
- MIGRATION_REPORT.md: file-by-file diff of what changed and why.

Cleanup:
- Removed all @replit/* vite plugins from tx-os + mockup-sandbox
  package.json + vite.config.ts + pnpm-workspace.yaml catalog.
- Removed @google-cloud/storage and google-auth-library from api-server.
- Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and
  *.tsbuildinfo build artefacts, scripts/post-merge.sh.
- Stripped Replit references from threat_model.md (now describes the
  self-hosted topology).
- New comprehensive .gitignore: Replit configs (.replit, replit.nix,
  replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/,
  .upm/), local storage/, .env*, build artefacts. .replit and replit.nix
  remain on disk (sandbox-protected) but will not ship to GitHub.
- scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD
  from env and throws in production if either is unset.

Drift from plan: replit.md was deleted (per off-Replit scope) so its
"do-not-touch files" preference list is moot. .replit/replit.nix kept on
disk only because they are sandbox-protected from edit/delete in this
environment, but they are git-ignored so they will not appear in a
fresh clone.
2026-05-13 14:08:11 +00:00

15 KiB
Raw Blame History

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 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 (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 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 .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.