# Security

Security model and hardening checklist for FXT CRM Mailer.

## Authentication & sessions

- Bcrypt password hashing (Laravel default), minimum 12-character passwords with letters
  and numbers enforced everywhere passwords are set.
- Login rate limiting: 5 attempts per email+IP per 5 minutes; password-reset and 2FA
  endpoints are throttled separately.
- Optional TOTP two-factor authentication (Google Authenticator compatible); secrets are
  stored encrypted; the challenge is rate-limited.
- Idle session timeout (default 60 min, `FXT_SESSION_TIMEOUT`), plus Laravel session
  lifetime. Sessions are database-backed, encrypted, `Secure`, `HttpOnly`, `SameSite=Lax`.
- Deactivating a user (`is_active = false`) kills access on their next request.
- Password resets go through signed single-use tokens; responses never reveal whether an
  account exists. Failed logins are audit-logged.

## Authorization

- spatie/laravel-permission with three roles. Every admin route is guarded by a specific
  permission middleware (`view contacts`, `send campaigns`, `manage settings`, …).
- Viewer is read-only; Campaign Manager cannot touch users, audit logs or settings.
- The last active Super Admin cannot be demoted, deactivated or deleted.

## Input handling

- CSRF protection on all state-changing admin routes (exempt: provider webhooks and the
  RFC 8058 one-click endpoint, which have their own verification).
- Eloquent/query-builder bindings everywhere — no raw user-controlled SQL. LIKE inputs
  are escaped (`%`/`_`).
- Mass-assignment protection via explicit `$fillable` on every model.
- All output escaped by Blade; personalization values are HTML-escaped when injected
  into email HTML.
- Email template HTML is sanitized server-side (DOM-based): removes `<script>`,
  `<iframe>`, `<object>`, `<embed>`, `<form>`, event handlers (`on*`), `javascript:`/
  `vbscript:`/non-image `data:` URLs and CSS `expression()` — while preserving
  email-compatible table/inline-style markup.

## File uploads (imports)

- Extension **and** MIME-type validation; size limit (default 20 MB, configurable).
- Stored under `storage/app/imports` with random UUID names — outside the web root,
  never executable, deleted after staging.
- Spreadsheet cells are cleaned at entry: control characters stripped, leading formula
  triggers (`=`, `@`, tab, CR) removed; CSV **exports** additionally prefix
  `= + - @`-leading cells with `'` (OWASP CSV-injection guidance).

## Email & tracking security

- Unsubscribe URLs use 64-char random tokens (unguessable, no DB ids exposed); the page
  masks the address; invalid tokens return 404 with no information leak.
- Tracking pixel and click redirects are HMAC-SHA256-signed with the app key. The click
  signature covers **message id + exact target URL**, so tampered links fall back to the
  site root — no open redirect, no forged analytics.
- Obvious bot/scanner user agents are excluded from open/click counts.
- Webhooks: secret URL token + SNS signature verification (SES) + HMAC verification
  (Mailgun) + idempotency by payload hash + rate limiting.

## Secrets

- Provider API keys entered in Settings are encrypted at rest (Laravel Crypt) and
  re-displayed masked (first 4 chars only). Audit logs mask sensitive keys.
- `.env` lives outside the document root; the shipped `.htaccess` additionally denies
  dotfiles and non-index PHP files. `APP_DEBUG=false` in production — no stack traces.

## HTTP hardening

Application middleware sets on every response:
`X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`,
`Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`, and HSTS on
HTTPS. `.htaccess` forces HTTPS.

## Audit trail

`audit_logs` records logins (success/failure), logouts, password changes/resets, 2FA
changes, all CRUD on contacts/lists/segments/templates/campaigns/users, campaign
launches with the compliance confirmation snapshot, suppression additions **and
removals (with mandatory reason)**, settings changes (masked), exports and test sends —
with user id, IP and user agent. Audit logs are viewable by Super Admin only and are
never deleted by the retention pruner's default configuration.

## Anti-abuse / compliance by design

- Global suppression: unsubscribed, hard-bounced, complained and manually suppressed
  addresses are excluded at queue-build time **and** re-checked immediately before every
  individual send. Imports can never reactivate them.
- Mandatory unsubscribe link + sender postal address (auto-appended footer when missing),
  `List-Unsubscribe` + one-click POST headers.
- Recorded 5-point compliance confirmation before any campaign can start.
- Automatic campaign pause at ≥5% bounce or ≥0.3% complaint rate (configurable).
- Individual sends only — the code path has no CC/BCC capability.

## Operational checklist

- [ ] `APP_DEBUG=false`, `APP_ENV=production`, unique `APP_KEY`
- [ ] `.env` not web-accessible (verify: `curl -I https://crm.fxtltd.co.uk/.env` → 403/404)
- [ ] `storage/` and `vendor/` not web-accessible
- [ ] `FXT_WEBHOOK_TOKEN` set to a unique 64-char value
- [ ] AutoSSL active and auto-renewing; HTTP→HTTPS redirect working
- [ ] `composer audit` clean on every update (clean as of this release)
- [ ] Database user has access to this database only
- [ ] 2FA enabled for every Super Admin
- [ ] Backups configured and restore tested (see BACKUP_AND_RESTORE.md)

## Reporting

Report suspected vulnerabilities privately to the site owner; do not open public issues.
