# Security

## Threat model highlights

The platform fetches **user-supplied URLs** (crawler, verification) and will
later **publish changes to customer websites**. The two dominant risks are
SSRF into our infrastructure and cross-tenant data access. Both have explicit,
tested controls.

## Controls in place (Phase 1–2)

### Authentication
- Passwords hashed with **argon2id** (19 MiB, t=2, p=1 — OWASP baseline).
- Password policy: ≥10 chars, ≥2 character classes, max 200.
- Session tokens: 256-bit random, stored **only as SHA-256 hashes**; cookie is
  `HttpOnly`, `SameSite=Lax`, `Secure` in production, 30-day expiry with
  server-side expiry enforcement.
- Account lockout: 8 failed logins → 15-minute lock. Counter resets on success.
- Rate limits on register/login/password-reset per IP.
- Password reset: single-use hashed tokens, 30-minute TTL; **all sessions are
  destroyed** on reset. Responses never reveal whether an account exists.
- Email verification: single-use hashed tokens, 24-hour TTL.
- 2FA: schema fields present (`twoFactorEnabled`, encrypted secret); TOTP
  enrolment lands with the settings page (tracked in TASKS.md — not claimed
  as done).

### Tenant isolation
- Every organisation-owned row carries `organisationId` (website rows also
  `websiteId`).
- All org-scoped routes pass through `requireOrgAccess()` which returns
  **404 for non-members** (no existence leak) and 403 for insufficient role.
- Website-scoped routes use `loadWebsiteForUser()` — lookup + membership +
  RBAC in one call, impossible to forget the check without skipping the
  helper (enforced in code review).
- Background jobs carry `organisationId` in their payload and only touch rows
  through the crawl's own IDs.

### SSRF (crawler & verification)
`src/lib/security/ssrf.ts` — every outbound fetch of user-supplied URLs uses
`safeFetch`, which:
- allows only `http:`/`https:`; rejects embedded credentials;
- blocks `localhost`, `*.localhost`, `*.internal`, metadata hostnames;
- resolves DNS first and rejects if **any** A/AAAA record is private,
  loopback, link-local, CGNAT, multicast, reserved, TEST-NET, benchmark or
  cloud-metadata (169.254.169.254) space — IPv4 and IPv6 including
  IPv4-mapped and NAT64 forms;
- follows redirects **manually**, re-validating every hop (blocks
  redirect-based rebinding);
- caps redirects (5), response size (5 MB), read time (20 s).

Covered by `tests/ssrf.test.ts`.

### Crawler etiquette
- robots.txt parsed and enforced (RFC 9309 semantics, longest-match,
  Allow-beats-Disallow); crawl-delay honoured (capped 60 s).
- Identifying user agent: `NorthWestSEOBot/1.0 (+https://seo.northwestcar.group/bot)`.
- Per-site page limits and per-request delay.

### Input & output
- All API bodies validated with Zod; consistent error envelope with request ID.
- Prisma parameterised queries only — no raw SQL string interpolation.
- React escapes output by default; no `dangerouslySetInnerHTML` in the app.

### Headers & indexing
- `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `X-Frame-Options: DENY`,
  restrictive `Permissions-Policy` (next.config.ts). HSTS is set at the
  reverse proxy after SSL is confirmed (see DEPLOYMENT.md).
- `/dashboard` and private routes are `noindex` and excluded in robots.txt.
- CSP: to be added alongside the first third-party script (Stripe); the
  policy will be built from actual dependencies, not `*`.

### Secrets & logging
- No secrets in source. `.env` is gitignored; `.env.example` has placeholders.
- pino redacts password/token/secret-shaped fields as a safety net; code never
  logs raw tokens or credentials by design.
- Integration credentials (future GSC/GA4/WordPress) are stored AES-256-GCM
  encrypted (`IntegrationCredential` model: ciphertext + iv + authTag) with
  the key in `CREDENTIAL_ENCRYPTION_KEY`.
- API keys (future) stored as SHA-256 hashes with an identifying prefix.

### Auditability
- `AuditLog` rows for register, login (success/failure), verification,
  website create/update/archive, crawl start/cancel — with user, org, IP and
  request ID. Audit failures never crash the primary action.

## Planned controls (later phases, tracked in TASKS.md)
- TOTP 2FA enrolment/verification UI.
- OAuth (Google/Microsoft) with state validation + PKCE.
- Stripe webhook signature verification + idempotency.
- Outgoing webhook HMAC signatures.
- CSRF double-submit token for any non-JSON form posts (current API is
  JSON-only + SameSite=Lax which blocks classic CSRF vectors).
- Redis-backed rate limiting for multi-instance deployments.
- Dependency scanning in CI (`npm audit` currently run manually).

## Reporting
Security issues: email seo@northwestcar.group. Do not open public issues.
