# Security guide
- Passwords: `password_hash()` / `password_verify()` (bcrypt/argon2). Never MD5.
- Email verification: pending customer accounts cannot log in or place orders until an expiring, one-time, hashed six-digit email OTP is verified; codes expire after five minutes, allow five incorrect attempts, and are rate-limited to five requests per email and ten per IP per hour with a 60-second per-account resend cooldown. The email/IP counters are serialized in MySQL to prevent concurrent requests bypassing limits.
- Contact-number changes keep the existing verified number until an OTP sent to the new number is successfully verified; profile and order queries are scoped to the authenticated customer ID.
- Email: registration delivery uses PHPMailer with authenticated Gmail SMTP over STARTTLS and server-side environment credentials. Keep the Gmail App Password out of source control and frontend code. Registration OTPs are never logged or displayed, and SMTP exception details are not included in application logs.
- Password recovery: only active, verified accounts can request a reset. Responses do not reveal whether an account exists; per-IP and per-account limits apply, and reset tokens are hashed, expire after 30 minutes, and are single-use. Production delivery requires configured SMTP; local fallback links are written only to the protected PHP error log.
- SMS: the optional HTTPS generic SMS adapter is used only for phone-number changes, not customer registration.
- Payments: customer online GCash uses Xendit's server-side Payments API when configured. The backend checks callback tokens and independently retrieves payment/request details, matching business, reference, amount, currency, country, GCash channel, and success status before creating an order. A browser redirect, screenshot, or customer-submitted reference is never proof.
- Roles: only active `admin`, `cashier`, and `customer` accounts can sign in. Cashiers are restricted to customer-submitted GCash receipt review; legacy staff accounts remain disabled.
- Payments: manual GCash orders remain `pending_verification` until an authorized Cashier checks the merchant account, confirms the unique reference and amount, and records approval or rejection. Counter orders remain historical and are not offered in customer checkout. Role checks, row locks, unique references, and transactions protect verification actions.
- Payments: receipt files are MIME-checked, size-limited, stored outside the public document root, and streamed only to administrators and the owning customer. Treat receipts and submitted references as untrusted supporting data.
- Payments: keep Xendit API keys, callback tokens, and merchant configuration in private server settings, use HTTPS for production/webhooks, and apply reviewed schema migrations before enabling checkout. Xendit is optional; enabled manual GCash/counter choices do not create a paid payment by themselves.
- Sessions: custom name, httponly, SameSite=Lax, regenerate on login.
- Login: the arithmetic CAPTCHA is required on every sign-in attempt when `ENABLE_CAPTCHA` is enabled, including the first attempt. Failed attempts remain in `login_attempts`. Accounts are not locked after incorrect passwords, avoiding account-lockout denial of service; login requests are temporarily rate-limited after 20 failed attempts from the same IP in a rolling 15-minute window. Deploy behind additional edge rate limiting.
- RBAC: `require_login()` + `require_role([...])` on every page.
- SQLi: PDO prepared statements everywhere, no string interpolation.
- XSS: `e()` escape on output; validation + trim on input.
- CSRF: token per session, checked on all POST.
- Audit: login/logout, create/update/delete, payment updates with user/date/time/IP.
- Deployment: force HTTPS, secure cookies, keep the installer unavailable outside local development, disable APP_DEBUG, backups + error logging.
