9.1 KiB
9.1 KiB
Threat Model — OMT / Recharge Cell Shop
Purpose. Document, before any code is written, every realistic way an employee (or a colluding pair) can steal money or goods from the shop, and the specific control the system must enforce to block each path. Every later migration, RLS policy, trigger, and UI rule must trace back to a row in this table.
This file is append-only in spirit: when a new theft vector is discovered, add a row; do not delete history.
Actors
| Actor | Description |
|---|---|
cashier |
Operates a till during a shift. Highest fraud-risk role. |
manager |
Approves voids, refunds, overrides. Can collude with cashier. |
owner |
Read-everything. Surprise inspections. Sets prices/fees. |
auditor |
Read-only third party (accountant). |
customer |
May be an accomplice (fake refund, fake cancellation). |
Trust boundaries
- Browser/POS ↔ Supabase: client is hostile. Never trust client-supplied prices, fees, FX rates, timestamps, user IDs, or shift IDs.
- App role ↔ Postgres: even the service role must not be able to
DELETEfrom the ledger or rewrite hashes. Enforce with table grants + triggers. - Shop ↔ Provider (OMT / Alfa / touch / Bank): the provider's statement is the source of truth for reconciliation. Any local row not present on the provider statement is suspect.
Theft vectors and required controls
| # | Vector | Control (must exist before go-live) | Enforced in |
|---|---|---|---|
| 1 | Pocket cash, never enter the transaction | Mandatory printed receipt, sequential reference_no per shop, customer SMS with reference, gap-detection report |
DB + server |
| 2 | Enter transaction, then edit / delete it after customer leaves | Append-only ledger; UPDATE/DELETE revoked; void requires reason + manager PIN; row hash chain |
DB triggers |
| 3 | Recharge own/friend's number flagged as "test" | No "test" flag exists; every recharge debits stock or e-float | Schema |
| 4 | Sell a scratch card, keep cash, claim "lost card" | Voucher serial scanned on intake and on sale; loss requires manager approval; variance assigned to cashier | DB + UI |
| 5 | Use shop OMT terminal to send to themselves at zero / reduced fee | Fee schedule server-side and immutable per cashier; OMT statement reconciliation; alert on cashier as sender or beneficiary | DB + recon job |
| 6 | Manipulate FX rate (e.g. "I gave 89,000 LBP/USD instead of 90,000") | fx_rates populated by scheduled job; cashier selects, never types; rate stamped on each txn |
DB + cron |
| 7 | Skim cash from drawer, blame "shortfall" | Blind closing count (declared before expected revealed); per-cashier variance trend; chronic shortage alert | UI + report |
| 8 | Collect customer cash, "deposit later", never deposit | Forced shift close before leaving; aging-outstanding alert > N hours; bank-deposit reconciliation | DB + recon |
| 9 | Share login with a colleague | Per-user PIN re-prompt, device binding, session timeout, no shared accounts | Auth |
| 10 | Print fake receipts on a second printer | Receipt carries server-signed QR (JWT of txn_id); spot-scan by owner returns DB row or "FAKE" |
Server |
| 11 | Refund/void to themselves | Void/refund requires manager PIN on same device; cashier cannot self-approve; daily void report per cashier | DB + UI |
| 12 | Sell recharge below price ("friend discount") | Price list server-controlled; cashier UI has no price field; override only by manager and logged | DB + UI |
| 13 | Take goods (phones/accessories) without sale | Per-shift inventory count; CCTV ↔ txn time sync; variance report | UI + ops |
| 14 | Tamper with the closing count | Declared count entered first and locked; expected revealed only after; both stored | DB |
| 15 | Accomplice customer "cancels" after cash handed | Cancellations require manager + reason + photo of voided receipt; CCTV cross-check | UI + ops |
| 16 | Structure large transfers under multiple fake walk-in identities | KYC threshold per (customer, day) and (beneficiary, week); customer record mandatory ≥ threshold | DB + AML report |
| 17 | Off-hours transaction when nobody is watching | Shift hours per shop; after-hours flag + alert | DB + alert |
| 18 | Manager–cashier collusion to mass-void real sales | Void rate per (cashier, manager) pair trended; owner-only weekly review; voids count against shift variance | Report |
| 19 | Replay an old OMT receipt to a new customer | external_ref unique per provider; duplicate detection on insert |
DB constraint |
| 20 | Cashier opens a second, undeclared till on the same device | One open shift per till_id; device fingerprint pinned to till |
DB + auth |
| 21 | Cashier marks recharge "failed at provider" and keeps cash | Provider e-recharge response stored as external_ref and reconciled; "failed" requires provider failure id |
DB + recon |
| 22 | Cashier "exchanges currency" at a worse rate than recorded, pocketing the spread | FX swap is a typed cash_movement with both legs at the system rate; deviation requires manager override |
DB |
| 23 | Cashier deletes their browser data to "lose" pending offline transactions | Offline queue persisted with server-issued idempotency key; missing key sequences flagged on reconnect | Client + server |
| 24 | Insider (developer/DBA) silently edits the database | Hash chain on ledger; daily hash anchor exported off-site; restricted DB roles; audit of all DDL and privileged SQL | DB + ops |
| 25 | Backdated transaction to fit a doctored shift count | occurred_at server-side now(); cashier cannot set; backdate only by owner role with reason |
DB |
Non-negotiables (no go-live without these)
- RLS on every table; no table is publicly readable or writable.
INSERT-only ledger with row hash chain;DELETErevoked from every role.- All money writes go through
SECURITY DEFINERPostgres functions, not raw table writes. - Server-controlled prices, fees, and FX rates. No cashier-typed money rules.
- Blind cash close per shift, with declared-vs-expected variance stored.
- External reconciliation (OMT, Alfa, touch, bank) before any month-close.
- Per-user account, device-bound, with PIN re-prompt for sensitive ops.
- Receipt with signed QR linking back to the ledger row.
Review cadence
- Every new feature PR must reference at least one row above (or add one).
- Quarterly walk-through: pick 5 random rows, demonstrate the control still works on a staging environment.