# Haven KE — Security Architecture

This is a static-site demo. In production, every layer below maps to a hardened backend service. The frontend already implements the parts that apply pre-network.

## Threat model

| Vector | Where it lands | Mitigation in this build | Production hardening |
|---|---|---|---|
| **SQL injection** | n/a (no SQL yet) | All string interpolation goes through `Security.esc()` before any `innerHTML` | Use parameterized queries only (e.g. Knex/Prisma). Never concatenate user input into SQL. Use a single DB user per service with least-privilege grants. |
| **XSS (reflected/stored)** | Search inputs, admin views, broker names | All user-generated strings are HTML-escaped via `Security.esc()` before render. CSP set to `default-src 'self'` (see `<head>`). | Server-side templating with auto-escape. Output encoding per context. Trusted Types where supported. |
| **CSRF** | Form submissions (submit property, KYC, payment) | Frontend uses a `Portal.Security.rateLimit()` per-action throttle. Real backend would issue CSRF tokens per session. | Same-Site=Lax cookies + double-submit token pattern. |
| **Auth bypass** | `isAdmin`, `isBroker`, KYC check | `Portal.isAdmin()`, `Portal.isBroker()` checks are server-validated in production. Currently these run client-side as a UX gate only — they are NOT security. | JWT with short expiry + refresh, server-side session store, RBAC. |
| **IDOR (Insecure Direct Object References)** | Listing detail, payment, deal closure | All listing/agent lookups go through `Portal.getAllListings()` which uses the local dataset. In production: server enforces ownership & authorization. | Always authorize against the requesting user. Never trust IDs from the client. |
| **File-upload abuse** | KYC docs, listing photos, video walkthrough | `Portal.Security.validFile()` enforces: max 25MB for docs, 200MB for video, MIME-type allow-list (jpeg/png/webp/pdf for docs, mp4/webm for video). | Server re-validates MIME via `file` magic bytes. Store in private bucket (S3/R2). Run ClamAV / similar. Generate thumbnails, never serve raw uploads. |
| **Video tampering** (the "non-paused" requirement) | KYC & listing walkthroughs | `Security.validateVideoDuration()` ensures ≥ 15s for KYC, ≥ 30s for listings. Live recording also has a frame-difference loop that flags "no motion for >3s" as a pause — paused recordings fail review. | Server-side re-encoding with ffmpeg to strip metadata + verify duration. Optional: liveness check (face present, not a static image). |
| **Privilege escalation** | `signUpAsAdmin` | Gated by a hard-coded `ADMIN_CODE` ("haven-admin-2025"). Acceptable for a demo. | Server-enforced RBAC. Invite-only admin accounts. Audit log immutable + append-only. |
| **Payment fraud** | M-Pesa / card pay flows | Client-side validation (Luhn-style for card, regex for M-Pesa). No card data ever stored client-side. | Daraja `/mpesa/stkpush/v1/processrequest` + signed IPN callback validation (HTTPS only). Card via Stripe/Flutterwave tokenization (PCI-DSS). 3DS for >KSh 5,000. |
| **Password / credential exposure** | Sign up form | Minimum 6-char password (client-side). Never stored in plain text in production. | bcrypt (cost 12+) or argon2id. Account lockout after 5 failed attempts. MFA via SMS OTP for admin & broker accounts. |
| **Sensitive data exposure** | KYC docs, landlord title deeds | Documents kept in `listingDocuments` with metadata only (filename, size, type). Actual blobs never serialized to localStorage. Admin sees metadata only, not the file. | Private object store, signed time-limited URLs for admin review only, encryption at rest with KMS-managed keys. |
| **Open redirect** | Not currently in flows | Auth redirects go to known internal sections only. | Validate `redirect_uri` against an allow-list. |
| **Mass assignment** | signUp, submitProperty | Explicit field allow-lists in functions; never spread raw request body. | Server-side DTOs that strip unknown keys. |
| **Clickjacking** | n/a (no iframe use) | `X-Frame-Options: DENY` should be set by the host. | CSP `frame-ancestors 'none'`. |
| **Rate limiting / brute force** | Sign up, KYC, pay | `Security.rateLimit()` 5/min per action. | Per-IP and per-account token-bucket at the edge. CAPTCHA after threshold. |
| **Information disclosure** | localStorage | Demo: state is plaintext, OK for local-only demo. | Encrypt at rest, never log secrets, generic error messages to users. |
| **Dependency vulnerabilities** | Leaflet, Google Fonts (CDN) | All dependencies pinned. | Snyk/Renew in CI. SBOM published. Subresource Integrity (SRI) for CDN scripts. |
| **Logging / PII** | adminLogs, deal records | Logs include names, emails, amounts. Acceptable for a controlled admin. | Strip PII before log shipping. Encrypt log storage. Retention policy. |

## Content Security Policy (set in `<head>`)

```
default-src 'self' 'unsafe-inline' data: blob: 
  https://*.tile.openstreetmap.org https://*.basemaps.cartocdn.com 
  https://fonts.googleapis.com https://fonts.gstatic.com 
  https://images.unsplash.com https://unpkg.com;
img-src 'self' data: blob: https:;
media-src 'self' blob:;
connect-src 'self' https:;
worker-src 'self' blob:;
```

**Production tightening**: remove `'unsafe-inline'` once the inline event handlers (`onclick="..."` in the newsletter form) are removed. Move to a `nonce-` based CSP.

## Input validation (frontend)

`Portal.Security` provides:

- `esc(s)` — HTML-escape for any user content
- `isEmail(s)` / `isPhoneKE(s)` — RFC5322 / KE phone
- `validFile(file, { allowed, maxSize })` — MIME + size allow-list
- `rateLimit(key, limit, windowMs)` — token-bucket
- `validateVideoDuration(file, minSec, maxSec)` — uses HTMLVideoElement metadata

## What still needs a backend before production

| Layer | Today | Production |
|---|---|---|
| Database | localStorage | PostgreSQL with row-level security, encrypted at rest, daily off-site backups |
| Auth | Plain localStorage | OAuth 2.0 (Authorization Code + PKCE), short-lived JWT (15 min) + rotating refresh tokens in httpOnly cookies, MFA for admin/broker |
| File storage | In-memory blob URLs | S3 / R2 with private ACL, signed URLs (5 min), server-side virus scan, ffmpeg re-encoding, image-stripped EXIF |
| Payments | Simulated | Daraja STK Push (B2C callback over HTTPS, signed), Stripe Checkout / Flutterwave (PCI-DSS scope) |
| Video verification | Client motion-detect loop | Server ffmpeg pass: duration check, pause detection via scene-diff, optional face-presence (liveness) |
| Rate limiting | Client token-bucket | Edge (Cloudflare / AWS WAF rules), per-IP + per-account, CAPTCHA fallback |
| Audit log | localStorage `adminLogs` | Append-only DB table, signed, shipped to immutable storage (S3 Object Lock) |
| Backups | n/a | PITR + daily snapshot, 30-day retention, restore tested monthly |
| Secret management | Hard-coded `ADMIN_CODE` | AWS Secrets Manager / HashiCorp Vault, rotated quarterly |
| Email/SMS | None | Verified SES / Twilio senders, DKIM/SPF, opt-out |
| Subresource integrity | n/a | SRI hashes for every `<script src="…">` |

## Reporting a vulnerability

Email `security@haven.ke`. PGP key on the website. We aim to acknowledge within 24h and patch within 7 days for high-severity.
