# 05 — Threat Model (focus: tenant isolation)

## 1. Assets
Tenant business data; client PII and health data (POPIA special personal information); financial records; credentials and keys; audit trail; backups; availability.

## 2. Actors
Anonymous attacker · malicious/curious member · malicious staff of tenant A (horizontal) · low-privilege staff trying to escalate (vertical) · malicious tenant owner targeting tenant B · compromised platform admin · compromised email/payment provider webhook sender · server-level attacker (shell/DB read) · careless operator.

## 3. Trust boundaries
Browser↔server · app↔DB · app↔SMTP/payment/off-site storage · web user↔cron/CLI · tenant realm↔platform realm · app process↔key material.

## 4. Tenant-isolation threats (T-ISO) and controls
| ID | Threat | Control | Test (Isolation suite) |
|---|---|---|---|
| ISO-1 | IDOR: change `/clients/{id}` to another tenant's | Lookup by `(tenant_id, public_id)` from context; 404 on miss | For every route with an ID param, request with tenant B's ID as tenant A user |
| ISO-2 | Forged `tenant_id` in body/query/header/cookie | Ignored; context from session membership only | Inject tenant fields on all POST/PUT routes |
| ISO-3 | Child references foreign parent (e.g. invoice line → other tenant's product) | Composite FKs; service validation | Direct SQL insert cross-tenant must fail |
| ISO-4 | Unscoped query in a repository | `TenantScopedRepository`, static scan, code review rule | Scanner test fails build |
| ISO-5 | Search/filter/pagination leaking totals or rows | Scoping in query builder, counts included | Compare counts across tenants |
| ISO-6 | Report/export includes other tenants | Export builders take `TenantContext` only; export file path namespaced | Seed two tenants, export, assert no foreign IDs |
| ISO-7 | File download of another tenant's document, or path traversal | Authorised streaming controller; DB lookup by tenant; random filenames; realpath check | Fetch foreign file ID, `../` payloads |
| ISO-8 | Queue job runs in wrong tenant | Job carries tenant_id, context rebuilt, payload IDs rechecked | Enqueue job with foreign object ID |
| ISO-9 | Webhook mapped to wrong tenant | Resolve tenant from provider account reference verified by signature secret per tenant/provider config | Replay event with swapped reference |
| ISO-10 | Session/tenant switch abuse | Switch requires an existing active membership; session rotated; permissions re-evaluated | Switch to tenant without membership |
| ISO-11 | Cache/ view fragment sharing across tenants | Cache keys include tenant_id; no shared authenticated cache; service worker never caches authed responses | Cache key audit test |
| ISO-12 | Email/template/placeholder leaks | Templates tenant-scoped; placeholder resolver limited to tenant context | Render with foreign client ID |
| ISO-13 | Unique-constraint side channel (e.g. SKU/email exists) | Uniques include tenant_id; global uniqueness only for user email, with uniform responses on registration | Probe duplicates across tenants |
| ISO-14 | Audit log readable across tenants | Filtered by tenant; platform audit separate | Query audit as tenant A |
| ISO-15 | Platform support access abuse | `support_grants` with reason/expiry, banner, audited, tenant-visible | Expired/unauthorised grant denied |
| ISO-16 | Subdomain/host header trust | Host only picks branding/hint; allowlist of known hosts; no trust in `X-Forwarded-Host` unless configured proxy | Host-header injection test |
| ISO-17 | Backup/restore mixes tenants | Backups are whole-system; tenant-level export is a separate filtered path; restore tested | Restore-test checklist |
| ISO-18 | Blind-index collisions/cross-tenant correlation | Blind index key derived per tenant | Same value in two tenants gives different index |
| ISO-19 | Shared browser origin: one tenant's stored XSS, localStorage or service worker reaches another tenant | Subdomain per tenant, host-only cookies, uploads on a cookieless host, sanitised SVG, strict CSP (03 §4) | Cookie-scope test; CSP header test; upload served with sandbox |
| ISO-20 | Tenant-switch handoff token replayed or stolen | Signed, single-use, 60 s, bound to user and target host, stored hashed | Replay, wrong-host and expired token tests |

## 5. STRIDE by surface (non-tenant specific)
| Surface | Spoofing | Tampering | Repudiation | Info disclosure | DoS | Elevation |
|---|---|---|---|---|---|---|
| Login/session | Credential stuffing → throttle, MFA, breached-password check optional | Session fixation → rotate | Audit auth events | Uniform errors | Throttle, lockout w/o permanent DoS | Role confusion → Gate |
| Forms/API | CSRF tokens, SameSite | Validation, prepared statements | Audit mutations | Output escaping, CSP | Request size & rate limits | Server-side permission on each action |
| File upload | — | Content sniffing, re-encode | Audit upload | Encrypt, authorised stream | Size/quota limits | No script execution (outside docroot) |
| Payments/webhooks | Signature verify | Idempotency, append-only | Provider event log | No PAN/CVV stored | Event queue | Refund approval limits |
| Email | SPF/DKIM guidance (09) | Template placeholder allowlist | Outbox history | Redacted logs | Queue caps, retry limits | Template edit needs permission |
| Backups | — | Checksums, signed manifest | Backup audit | Encrypted archives, off-site key separate | Time budget, chunking | Restore is platform-only |
| Cron/CLI | CLI-only guard (`php_sapi_name`) | Locks | Heartbeat | — | Lock + budget | No web trigger |
| Service worker | Scope restricted | Versioned cache | — | Never cache authed data | Cache size cap | — |

## 6. Application-compromise scenarios
- **SQL injection:** prepared statements only; no dynamic identifiers from user input (allowlist mapping for sort columns); DB user lacks FILE and SUPER; separate low-privilege DB users.
- **XSS:** auto-escape by default; CSP with nonces; no inline event handlers; rich-text template HTML sanitised by allowlist.
- **Server compromise:** application-layer encryption protects against DB-only theft (dump, backup leak, SQL injection reading), **not** against an attacker who runs code in the app (keys are reachable). Mitigations: key file permissions, separate OS user, short-lived key exposure is not possible in PHP; document honestly (06 §7).
- **Resource exhaustion through password hashing:** Argon2id is memory-hard, so a burst of login attempts can exhaust the PHP-FPM pool. Controls: throttle before hashing, cap concurrent hash operations per pool, tune memory cost on the real server, and a cheap pre-check (CAPTCHA-free proof-of-work is a documented option) when the failure rate spikes.
- **Account lockout abuse:** throttling is per (account, IP) with backoff rather than a hard lock, so an attacker cannot lock out a victim from everywhere; legitimate users get an email-based unlock path.
- **Supply chain:** pinned Composer versions, `composer audit` in dev, minimal dependency set, vendor checksum in release manifest.
- **Insider (platform admin):** MFA mandatory; support grants audited and visible; no ability to read C3/C4 without a grant.

## 7. Abuse cases for business logic
Double-booking race · overbooking waitlist · replayed payment webhook · negative quantities/prices · refund above paid amount · refund by the approver of own request (blocked) · member freezing to dodge billing · discount stacking · stock going negative via concurrent sales · invoice number gaps manipulation · backdated check-ins by staff (audited, permissioned) · client-owned data export of someone else's data.

## 8. Residual risks (accepted/documented)
Shared server blast radius (owner controls hosting, so can harden: separate PHP-FPM pool/user, open_basedir) · no malware scanning by default · email delivery not guaranteed · PWA/iOS limits.
