# security.md

## Security Principles

### 1. Triple-verification on critical flows
- Billing transactions: client confirms, server validates, Stripe confirms (webhook).
- Permission changes: role-checked at controller, repository-checked at query, audit-logged after.
- Simulation runs: quota-checked, credit-deducted, persisted, then event-dispatched.

### 2. Input sanitisation at every entry point
- POST/GET data is never trusted. Controllers cast to expected types (`(int)`, `(float)`, `(string)`) before passing to repositories.
- File uploads (CSV/XLSX): MIME-sniffed, size-capped, written to a non-web-accessible temp dir, deleted after ingest.

### 3. SQL injection prevention
- **PDO native prepared statements** (`ATTR_EMULATE_PREPARES => false`). Every query is a bound prepare.
- The one exception (`LIMIT n` can't be a bound param under native prepares) sanitises to an int and inlines: `$limit = max(1, min(200, $limit)); ' LIMIT ' . $limit`.
- No string concatenation of user input into SQL anywhere.

### 4. Session control for authentication
- Session-based auth (`$_SESSION`).
- Cookies: `HttpOnly`, `Secure`, `SameSite=Lax`.
- Session ID regenerated on login.
- 30-minute idle timeout; rolling renewal on activity.
- Logout invalidates session server-side; cookie cleared.

### 5. Tenant isolation
- **Structural**: every repository extends `BaseRepository` which carries `$tenantId` from the session. Every query includes `WHERE tenant_id = ?`.
- **No cross-tenant read or write is possible** via the data layer. Tenants cannot be elevated by tampering with form fields.

### 6. CSRF protection
- POST forms include a CSRF token (session-bound). Middleware validates before controller.
- API routes use same-origin + CSRF token in header for state-changing operations.

### 7. Output escaping
- HTML output: `htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8')` in views.
- SVG output: `htmlspecialchars(..., ENT_QUOTES | ENT_XML1, 'UTF-8')` in `SvgChart`.
- JSON output: `json_encode()` with `JSON_UNESCAPED_UNICODE` only when necessary.

### 8. Permissions / authorisation
- Permission strings: `scenario.*`, `simulation.run`, `report.view`, `funds.*`, `admin.settings`, `billing.*`.
- Enforced at controller entry: `AuthMiddleware::requirePermission('simulation.run')`.
- Role grants permissions: `admin` gets everything, `adviser` gets client/scenario/simulation, `support` gets read-only.

### 9. Audit log
- Every sensitive action recorded with tenant_id, user_id, ip, user_agent, target_type, target_id, timestamp.
- Append-only; no DELETE statement against `audit_log` exists in the codebase.
- Retained for 6 years (FCA-regulated firms minimum).
- Satisfies UK GDPR Art.5(2) accountability requirement.

### 10. Encryption
- PII fields (`clients.name_enc`, `clients.ni_number_enc`) use `EncryptedField` wrapper with AES-256-GCM (UK GDPR Art.32).
- Encryption key in `config/database.php`, kept out of git via `.gitignore`.

---

## Compliance

### UK GDPR / Data Protection Act 2018
- UK GDPR consent flag on `clients.ukgdpr_consent` required before any saved client.
  (`popi_consent` column retained for migration backward compatibility.)
- Lawful basis: Art.6(1)(a) consent; Art.6(1)(b) contract performance for billing.
- Consent-withdrawn event purges PII (`ConsentWithdrawn` event handler — Art.7(3) right to withdraw).
  Audit record written to `gdpr_consents` table on withdrawal.
- Tenant DPO email stored on `tenants.gdpr_dpo_email`.
- Audit log retained for the regulatory minimum.
- Data is processed in the UK (cPanel host in UK datacentre).
- Right to erasure (Art.17) handled via `ConsentWithdrawn` event: `name_enc`, `ni_number_enc`, PII fields nulled.
- Data Subject Access Requests (DSARs) supported via audit log query.

### FCA Conduct of Business (COBS)
- Simulation outputs disclose model assumptions and limitations in every PDF report (FCA disclosure block).
- Strategy recommendations carry "the adviser remains responsible" banner (Phase 2 UI).
- Run history is immutable for audit (clients can request, advisers can produce).
- Every PDF report includes FCA-aligned disclaimer; CostusWorx is a modelling tool, not regulated advice.

---

## Hosting Constraints (security hardening for shared hosting)

- **No SSH** → no Composer in production. Vendor folders for mPDF and similar must be uploaded as zip.
- **No `.env` library** → secrets live in `config/*.php` with `.gitignore` entries. Files are PHP so they execute (not served as plain text).
- **API keys** (e.g. `OPENFIGI_API_KEY`, `STRIPE_SECRET_KEY`) are set via `SetEnv` in `public/.htaccess`. Apache makes them available to PHP via `getenv()` only — they are never served as HTTP responses. `.htaccess` with real keys must never be committed to git; maintain `.htaccess.example` without values for version control.
- **No firewall control** → mitigations: no admin URLs in robots-discoverable paths, IP allowlists for admin if needed (cPanel-level), HTTPS enforced via host config.
- **Shared memory / files** → PHP sessions are file-backed by default; tenant data files are never written to web-accessible paths.
- **Open `phpinfo.php` etc.** must not exist in webroot — verified per deploy.
- **Admin portal isolation** → `/admin/*` routes require `$_SESSION['admin_id']` (set only by `AdminAuthController`). Tenant session (`$_SESSION['user_id']`) grants no admin access. The two sessions are entirely separate.

---

## Known security work still open (carried in `risks.md`)

- **CSP headers** — not yet emitted; planned for Phase 5.
- **2FA / TOTP** — not yet implemented; planned for paid tiers in Phase 5.
- **Stripe webhook signature validation** — must verify `Stripe-Signature` header on `/billing/notify` using `STRIPE_WEBHOOK_SECRET`. Code stub present; production audit pending.
- **`async_funds_agent.php`** — legacy file exposing DB creds was identified in earlier audit. **Should be deleted from webroot.**
- **Vendor zip provenance** — when mPDF is installed manually, ensure it's pulled from the official `mpdf/mpdf` GitHub release, hash-verified.
