# Security Checklist

Defense in depth - never trust the UI for security; enforce at request entry, query layer, and database constraint levels.

## 1. Authentication

- [x] Bcrypt cost 12 via PHP's `password_hash` / `password_verify`.
- [x] Min 10 chars, complexity enforced (`PASSWORD_*` constants in `includes/config.php`).
- [x] Force password change on first login (`must_change_password = 1`).
- [x] Account lockout after `LOGIN_MAX_ATTEMPTS` failed attempts within a 15-minute window. Lockout duration controlled by `LOGIN_LOCKOUT_MINUTES`.
- [x] Generic "Invalid email or password" message - never leaks which side failed.
- [ ] Email-OTP 2FA mandatory for client users; optional for internal *(infrastructure exists in users.two_factor_enabled flag; OTP delivery to be added)*.
- [x] Session: `HttpOnly`, `SameSite=Strict`, `Secure` (under HTTPS). IP and user-agent bound; both checked on every request.
- [x] Session timeout: 30 min internal / 15 min client (idle, rolling).
- [ ] Force re-authentication for sensitive admin operations *(role changes, replace_all uploads, sharing) — currently uses CSRF + audit; full re-auth gate to be added*.

## 2. Authorization

- [x] `require_login('internal')` / `require_login('client')` at the top of every page.
- [x] Permission resolution: SysAdmin -> per-report `report_access` row -> role permissions -> deny.
- [x] Hard isolation: client users can only see reports under their own `client_id`. Database `CHECK` constraint on `users` table enforces `(user_type='client' AND client_id NOT NULL)` xor `(user_type='internal' AND client_id IS NULL)`.
- [x] Sysadmin "view as user" feature uses the same auth surface as a real session - no parallel privilege path.

## 3. Input validation & injection prevention

- [x] All queries use PDO prepared statements - `db()->prepare(...)->execute([...])`. No string concatenation anywhere.
- [x] CSRF tokens on every POST form (`csrf_field()` + `csrf_check_or_die()`).
- [x] Output escaped with `e()` (which is `htmlspecialchars(..., ENT_QUOTES, 'UTF-8')`).
- [x] File uploads: extension whitelist (`xlsx`, `xls`, `csv`); max size enforced; renamed on save with random suffix; stored under `storage/uploads/{report_id}/`.
- [x] Excel formulas in input cells are read as cached values, never evaluated (the included `xlsx-reader.php` returns the stored `<v>` element).
- [ ] CSV-injection mitigation on export (prefix risky cells with apostrophe) - to be added on Excel export when implemented.

## 4. Output / response hardening

- [x] `.htaccess` blocks direct web access to `includes/`, `sql/`, `docs/`, `bin/`, `storage/`.
- [x] `.htaccess` blocks dotfiles, backups, batch files, sql, gitignore, log files.
- [x] Security headers: `X-Frame-Options DENY`, `X-Content-Type-Options nosniff`, `Referrer-Policy strict-origin-when-cross-origin` (in root `.htaccess`).
- [ ] HTTPS-only + HSTS header (per the production Nginx sketch in `deployment.md`).

## 5. Logging & audit (compliance grade)

- [x] Every login attempt logged in `login_logs` (success + failure + lockout).
- [x] Every config change logged in `audit_logs` with full before/after JSON diff.
- [x] Every download logged in `download_logs` (user, report, output, format, IP).
- [x] Every upload logged in `upload_logs` (state transitions).
- [x] Logs are read-only in the UI - no delete button anywhere. Retention enforced via `audit.retention_days` setting (default 730).

## 6. Secrets

- [x] Database credentials in `includes/config.php`. Not committed to git via `.gitignore` if you put credentials in a separate file.
- [ ] SMTP / S3 credentials should be moved to environment variables in production.

## 7. Backups

- [ ] Daily automated DB dump (per `deployment.md` cron). Restore drill quarterly.

## 8. Dependency hygiene

- [x] Zero PHP package dependencies. Native PDO, ZipArchive, SimpleXML, password_hash. The Tabler.io and Chart.js assets are loaded from CDN; in production consider self-hosting them.

## 9. Pre-prod manual checks

Before flipping live for client traffic, verify these manual attempts fail:

- [ ] Switching `client_id` in URL/session as a client user -> 404 (the client/report.php view checks ownership).
- [ ] Uploading an `.xlsx` with embedded macros -> ignored (xlsx-reader does not execute).
- [ ] Replaying a logged-in cookie after logout -> invalid (session_destroy + cookie clear).
- [ ] Brute-force login (>5 fails) -> account lockout triggered.
- [ ] Direct GET on `/includes/auth.php` -> Apache returns 403 (Require all denied).
- [ ] Sysadmin "view as" then admin action -> the action runs as the impersonated user (correct expected behaviour).
