# Security checklist

Legend: ✔ implemented and covered by an automated test · ◐ implemented, not covered by an automated test · ☐ operator action

## Application
- ✔ Passwords hashed (bcrypt by default; set `BCRYPT_ROUNDS`/switch driver to argon2id in `config/hashing.php` if your PHP has it) · min length 10 · ✔ password change revokes other sessions
- ✔ CSRF on all web forms (API routes are token-authenticated) · ◐ Blade escaping everywhere (`{{ }}`), no raw output of user data
- ✔ Tenant isolation: tenant derived from session user / API credential only (tests: cross-client 404s)
- ✔ RBAC via `App\Support\Permissions` + `staff:` middleware; provider/pricing/staff/releases are super-admin only
- ✔ Plugin API keys: random 48 chars, SHA-256 hashed at rest, shown once, revocable; per-website; regeneration needs password
- ✔ Provider secrets encrypted at rest (Laravel `encrypted` cast = AES-256-CBC+HMAC with `APP_KEY`), hidden from serialization, never rendered (only last 4 chars), never sent to plugin/clients
- ✔ Call phone numbers encrypted at rest; dashboards/API show masked numbers only
- ✔ Webhooks: HMAC-SHA256 over raw body, `hash_equals`, optional signed timestamp, event dedupe, unmatched events stored not applied
- ✔ SaaS→plugin pushes signed (timestamp+nonce+body), plugin enforces window and one-time nonces (stub-tested)
- ✔ Wallet: DB transactions + `lockForUpdate`, append-only ledger (model-level), reservations released/captured exactly once
- ✔ Payment callbacks only via server-to-server verification; amount must match; manual gateway needs staff approval; no browser-redirect crediting
- ✔ SSRF guard on website verification (private/reserved IPs refused; `VOICEORDER_ALLOW_PRIVATE_HOSTS` must be `false` in production)
- ✔ Rate limiting (login 8/min, API public/site/webhook limiters, contact form, register)
- ✔ Plugin ZIP upload validation (zip, size, `voiceorder/` root, no traversal, header version == entered version), stored on the private disk, signed 10-min download URLs
- ✔ Audit log for financial adjustments, provider changes/activation overrides, client status, website decisions, releases, settings (reason mandatory)
- ✔ Anti-abuse for free credits: normalised-email + phone fingerprints, disposable-domain list, per-IP daily cap, unique DB key per client
- ◐ Secure session cookies: set `SESSION_SECURE_COOKIE=true`, `SESSION_DRIVER=database` (session revocation works by deleting rows)
- ◐ Production errors: `APP_DEBUG=false`; API returns generic 500 JSON without internals

## Operator (☐)
- ☐ HTTPS everywhere; HSTS at the web server · ☐ `.env` outside web root / denied · ☐ remove `VOICEORDER_ADMIN_PASSWORD` after seeding
- ☐ Back up DB **and** `APP_KEY` · ☐ restrict cPanel/SSH access, keep PHP updated · ☐ separate DB user with only this DB
- ☐ Rotate provider API keys if staff leave · ☐ review Audit log and Webhook events regularly · ☐ configure SPF/DKIM
- ☐ Penetration test / dependency audit (`composer audit`) before large-scale launch – not performed here

## Known gaps / honest notes
- Ledger immutability is enforced in application code, not by DB triggers.
- No 2FA for staff in this version.
- Free-credit fraud controls are heuristic; a determined user with many emails/phones/IPs can still farm credits. Phone SMS-OTP verification is the recommended next step.
- The ownership check proves control of the site's REST API, not of the DNS zone.
