Skip to content

Threat Model

This page summarises the security threats Mitch‑Risk is designed to mitigate, helping security architects and assessors evaluate the platform's security posture.

What We Protect

AssetSensitivityLocation
Vendor names, contacts, tiersConfidentialPostgreSQL (encrypted at rest via database, not application)
Assessment responses (answers, evidence)ConfidentialPostgreSQL + file storage (local/S3/Azure)
Staff credentials (password hashes, SSO links)CriticalPostgreSQL (bcrypt-hashed passwords, AES-256-GCM secrets)
Portal tokens and passwordsSensitivePostgreSQL (SHA-256 hashed tokens, bcrypt hashed portal passwords)
Audit log (who did what, when)SensitivePostgreSQL (pruneable, configurable retention)
Uploaded files (evidence, certs, attachments)ConfidentialFile storage (authenticated serving, MIME validation)

Trust Boundaries

  ┌─────────┐   HTTPS    ┌──────────┐   HTTP    ┌──────────────┐   TCP    ┌───────────┐
  │ Browser │◄──────────┤  Reverse  │─────────►│  Next.js App │────────►│ PostgreSQL│
  │ / API   │           │  Proxy    │           │  (port 3000) │         │  (port    │
  │ Client  │           │ (Caddy/   │           │              │         │   5432)   │
  └────┬────┘           │  nginx)   │           └──────┬───────┘         └───────────┘
       │                └──────────┘                   │
       │  Portal link                                       │   ┌──────────────┐
       │  (no-login token)                                  ├──►│ File Storage │
       │                                                    │   │ (local/S3/   │
  ┌────▼────┐                                              │   │  Azure)      │
  │ Vendor  │                                              │   └──────────────┘
  │ Portal  │                                              │
  └─────────┘                                         ┌────▼───────┐
                                                       │ SMTP Relay │
                                                       │ (outbound) │
                                                       └────────────┘
BoundaryProtocolAuth RequiredNotes
Browser → Reverse ProxyHTTPSSession cookie or API keyTLS terminated at proxy
Reverse Proxy → AppHTTPNone (trusted network)Internal Docker network
App → PostgreSQLTCPPassword (connection string)Internal Docker network
App → File StorageAPIS3/Azure credentials or filesystemCredentials AES-256-GCM encrypted
App → SMTPTLS (port 587)SMTP AUTHPassword encrypted at rest
Vendor → PortalHTTPSOpaque token in URLNo login, optional password gate

Threat Matrix

ThreatAttack VectorMitigationSeverity
Credential stuffingRepeated login attemptsRate-limited at 10/min per IP. bcrypt 12 rounds — computationally expensive even on successMedium
Session hijackingCookie theft via XSS, man-in-the-middlehttpOnly + secure cookies. Nonce-based strict-dynamic CSP blocks inline scripts. TLS provides transport securityLow
XSS (reflected/stored)Malicious script in vendor answers, template fields, or email tokensReact/JSX auto-escapes HTML. CSP script-src 'nonce-...' 'strict-dynamic' — no inline scripts or eval allowed in production. File upload MIME deny-list rejects SVG and HTMLLow
CSRFCross-site requests to state-changing endpointsServer Actions use POST with built-in CSRF protection. API routes require explicit auth header. CORS not configured (same-origin)Low
SQL injectionMalicious input in query parameters, CSV imports, API payloadsAll database access through Prisma parameterised queries. No raw SQL strings. Zod validates all external input before persistenceLow
API key compromiseLeaked or stolen bearer tokenbcrypt-hashed in database (never stored plaintext). Prefix-indexed lookup prevents full-table scan. IP allowlisting via CIDR blocks unauthorised IPs. Configurable expiry. Rate-limited per IP and per key prefix. Keys disabled instantly from SettingsLow
Portal token guessingURL traversal or brute force256-bit random token (2^256 search space). Rate-limited page loads (30/min per visitor). Server-enforced expiry. Opaque token is SHA-256 hashed for DB storageLow
File upload attackMalicious file upload (malware, polyglot, oversized)MIME type deny-list: HTML, JS, SVG, PHP, XML, executables, shell scripts. Configurable max upload size (default 20 MB). Configurable extension allowlist. File storage path uses resolve() + startsWith() to prevent traversal. Rate-limited uploads on portal (10/min)Medium
Secret exposureDatabase dump, environment leak, or config file accessSMTP password, SSO client secrets, cloud storage credentials encrypted at rest with AES-256-GCM. Encryption key in environment variable only. Secrets never returned to client (masked in API responses). DB settings returns boolean for password presence, not the valueLow
Privilege escalationUnauthorised access via role/permission manipulationFour-layer RBAC: page access (server component), UI gating (controls hidden not greyed), server action gating (permission check before write), API route gating (bearer auth + permission). Custom roles cannot self-escalate. Admin role lockedLow
Timing attackTiming-based secret comparison (CRON_SECRET, password, token)CRON_SECRET compared with crypto.timingSafeEqual(). bcrypt comparison is constant-time by design. API key prefix lookup + bcrypt verify (not raw string match)Low
SSO lockoutIdentity provider unavailable, all staff locked outBreak-glass URL (/login?break-glass=<token>) re-enables local login. Token bcrypt-hashed in DB, expires in 24 hours, single-use (consumed on first use). Rate-limited (10/min per IP)Low
Replay attackCaptured HTTP traffic replayedTLS enforced at reverse proxy (HTTPS). secure cookie flag prevents cookie transmission over plain HTTP. JWT session is stateless (server does not maintain session state to correlate replays). API keys can be revoked instantlyLow
Information disclosureSensitive data in logs, error messages, or API responsesFile evidence served only through authenticated route (never public URLs). API error wrapper (runApiHandler) catches unexpected errors → generic 500, logs details server-side only. Zod validation errors return structured messages without exposing internalsLow
Denial of serviceResource exhaustion via excessive requestsFixed-window rate limiter on login, portal access, file uploads, submissions, password reset, break-glass, and API endpoints. Configurable limits per concern. Max 100,000 tracked keys with per-namespace 5K cap; rejects when fullMedium
Dependency vulnerabilityKnown CVE in third-party libraryCI pipeline runs lint + typecheck + build on every push. npm audit on dependencies. Node 22 LTS pinned. All dependencies are regular (not optional peer) for reproducible installsLow

What We Don't Protect Against (Out of Scope)

  • Physical access to the server — the threat model assumes the deployment environment is physically secured
  • Root compromise of the host — database or file storage accessible to anyone with root on the Docker host is fully readable
  • Insider threat by Admin — the Admin role has full access to all data. No audit trail suppression prevention
  • MFA — MFA is enforced at the IdP level for organisations using SSO (Microsoft Entra ID, Google Workspace, generic OIDC). Local accounts are for small single-user or evaluation deployments; SSO users inherit the IdP's full MFA/conditional-access posture
  • Horizontal scaling — the in-memory rate limiter shares no state across containers. Multi-container deployments should use a shared Redis store (not yet implemented)

Open-source, self-hosted third party vendor risk management.