Files
TX/threat_model.md
T
Riyadh b66660e0f1 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, with mockup-sandbox under a
  `dev` profile (off by default). 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, plus
  a residual-risks section enumerating the 7 Medium + 8 Low backlog
  items from .local/security/manual-review.md and the unmigrated
  object-data note.

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, replit.md.
- Stripped Replit references from threat_model.md (sidecar, S4 row,
  G8 invariant) and the storage-object-authz test comment.
- New comprehensive .gitignore: attached_assets/, Replit configs
  (.replit, replit.nix, .replitignore, replit.md), agent state (.local/,
  .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*,
  build artefacts.
- 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, .replitignore, replit.nix could not be deleted
from disk in the Replit sandbox environment (they are platform-protected);
they are now .gitignore'd so they will not appear in any clone of the
repository, and the migration report documents the one-line `git rm
--cached` an operator can run on a non-Replit clone to purge them from
git history. replit.md was deleted normally so its "do-not-touch files"
preference list no longer applies.
2026-05-13 14:12:40 +00:00

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