> 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.
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. |
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.
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).
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.
| 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 |
| 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 |
- **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.