# Security notes

## Threat model in one paragraph
Anonymous internet users try to read other people's tokens/history, guess tokens or recovery keys, double-spend a token, abuse
race conditions, forge payments, or reach the admin console; a database-only leak should not reveal usable tokens; a provider
timeout must never create a duplicate transfer.

## Controls (and where)
| Risk | Control | Code |
|---|---|---|
| Reading other visitors' data | every query takes the visitor id from the **server session**; foreign refs give 404; no endpoint accepts an email/serial/sequential id as proof | `Repositories/*::findOwned`, `ActivityController::query` |
| Session theft / fixation | HttpOnly+Secure+SameSite cookies, strict mode, regeneration on identity change, idle + absolute expiry; separate admin cookie (Strict) | `AnonymousSessionService`, `AdminAuthService` |
| CSRF | per-session token header + Origin check on every POST/PUT | `CsrfMiddleware` |
| XSS | no `innerHTML`/eval anywhere in JS (CI check); CSP `script-src 'self'`, no inline scripts; PHP templates escape with `e()` | `Response::securityHeaders`, `tests/js/static-check.mjs` |
| SQL injection | PDO prepared statements, native prepares, strict mode | `Support/Database` |
| Token guessing | 80-bit `random_int` tokens; failure-counted lockouts per visitor and IP; per-IP/visitor throttles | `TokenService`, `RateLimitMiddleware` |
| DB-only leak | tokens/credentials stored as **keyed HMAC** (key in `.env`); vault is sodium secretbox with a derived key; recipients encrypted | `Support/Crypto` |
| Recovery abuse | 150-bit key shown once, hashed, failure-counted lockout, identical error for unknown/malformed | `RecoveryService/Controller` |
| Double redemption / races | atomic `UPDATE ... WHERE status='active'` reservation; DB unique `blocking_token_id`; idempotency keys per visitor; row locks on orders | schema, `FundingService`, `PaymentService::evaluate` |
| Double credit of a payment | unique `(network, txid, transfer_index, asset)`; idempotent evaluation; monotone confirmations | `payment_transactions`, `recordObservation` |
| Fake payments | only chain data (or signed webhook re-checked on-chain); asset contract + address + network + amount + confirmations verified; screenshots/claims never accepted; manual approval re-verifies the txid on-chain | `PaymentService`, `Chain/*` |
| Float errors | DECIMAL columns, BCMath strings, quotes rounded up | `Support/Decimal` |
| Provider timeouts | no DB transaction during the external call; unknown != failed; reconcile by client reference; never auto-retry | `FundingService` |
| Admin takeover | Argon/bcrypt via `password_hash`, constant-time dummy verify, per-IP and per-email failure lockout, optional TOTP, session regeneration, role checks on **every** route, CLI-only setup that locks itself | `AdminAuthService`, `routes/api.php` |
| Tampering with history | trigger-enforced append-only audit log, no deletes of financial rows, succeeded transfers immutable; FKs are `RESTRICT`, never `CASCADE` | `database/hardening.sql` |
| Secret leakage | `Logger`/`AuditService` redact token-shaped strings, credentials and secret-like keys; admin API never returns secrets (booleans only) | `Support/Logger` |

## Operator responsibilities
* Keep `.env`, `storage/`, `database/` outside the document root; `chmod 600 .env`; HTTPS only; restrict DB grants.
* Back up the DB **and** `APP_KEY` separately; rotating `APP_KEY` invalidates stored tokens/credentials (plan a migration first).
* Never store private keys or seed phrases anywhere in this system: receiving addresses are public, spend from a wallet you control offline.
* Monitor `audit_logs`, the admin "Needs attention" panel (late payments, unmatched transfers, uncertain funding) and `storage/logs`.
* Late / partial / duplicate payments: see README "How the money logic works". They are recorded, flagged, and resolved by an admin with on-chain evidence.
* The Content-Security-Policy allows `cdnjs.cloudflare.com` for Font Awesome. Vendor it locally and remove that origin for a stricter policy.

## Known limitations
* PHP code and SQL were statically checked but not executed before delivery: run the suites yourself (README checklist).
* `cURL` CA bundle must be current; TLS verification is always on.
* Rate limits are fixed-window counters in MySQL (fine for shared hosting; use a WAF/CDN for volumetric abuse).
* Anyone holding a token can redeem it (bearer semantics, by design: tokens can be gifted). Users are told to treat them like cash.
