Threat model
Adversaries we model, attack surfaces we protect, and the controls in place.
This is a working threat model for Unphish v2. It is not a SOC 2 audit document; it is a developer-and-reviewer-friendly enumeration of what we worry about and what controls we rely on.
Adversaries we model
| Adversary | Motivation | Typical capability |
|---|---|---|
| Phishing operator under enforcement | Avoid takedown; harvest more credentials. | May submit malicious content to evidence (bait page tries to inject scripts/exploits); may attempt credential phishing of analysts. |
| Threat actor running impersonation campaigns | Continue campaigns; identify and evade Unphish detection. | May plant evidence designed to confuse classification; may abuse client API to submit decoys; may probe for source-state leaks. |
| Cross-tenant attacker (malicious customer) | Read another customer's cases, brands, or evidence. | Has legitimate access to one tenant; tries to access another via API parameter manipulation, predictable IDs, or session reuse. |
| Cross-partner attacker (malicious partner operator) | Access competitor partner's clients or analyst notes. | Has admin in their own partner organization; tries to access another's. |
| Insider (Unphish staff or Engram developer) | Access customer data outside legitimate operations. | Has legitimate but scoped access; may try to act outside their role. |
| Insider (compromised credentials) | Same as above without intent. | Adversary has stolen a staff/customer cookie or credential. |
| Network/infrastructure attacker | Intercept traffic; pivot between services. | Reads or modifies traffic between components. |
| Supply-chain attacker | Inject code via dependencies. | Compromises an npm package or upstream dependency. |
| Provider / integration attacker | Trick the platform into submitting bogus takedowns. | Forges or manipulates provider responses. |
Surfaces and controls
Authentication
| Risk | Control |
|---|---|
| Credential stuffing / brute force | Authentik handles authentication with rate limiting, MFA enforcement at the org level, and account lockout. The app has no password endpoint at all. |
| Session fixation | Session cookies are issued post-OIDC-callback with new identifiers; signed with HMAC; HttpOnly; Secure in production; SameSite Lax. |
| Token leakage | OIDC tokens are not stored in the app database. Access tokens are short-lived; refresh tokens stay at Authentik. |
| Compromised admin token | AUTHENTIK_ADMIN_TOKEN rotation is documented; Authentik emits secret_rotate events; Hub flags fingerprint mismatches. |
| Phishing of analysts | Branded sign-in flow; URL is consistently unphish-staging.engram.org or unphish-prod.engram.org; staff trained to verify host before entering credentials. |
Authorization
| Risk | Control |
|---|---|
| Cross-tenant read | Every tenant-scoped query includes organization_id (and client_id where applicable) at the data-access layer. Cross-tenant access is impossible by construction. |
| Cross-tenant write | Same as read; mutations also write organization_id from the active session, never from request input. |
| Privilege escalation via role manipulation | Role values are normalized server-side before persistence; database constraints reject unknown values; UI labels are display-only. |
| Privilege escalation via system_role | system_role is set by admin-only flows or auto-promotion based on internal email domain; it is never settable from a customer-facing surface. |
| Insider abuse via impersonation | Support preview is capability-gated; banner-visible; time-bounded (60-minute max); requires explicit reason; every action is double-audited. |
| API key scope bypass | API keys are scoped at issuance; the API gateway enforces scope on every request and on every response field. |
| Brand-level access bypass | Reviewers with brand restrictions cannot approve other brands' cases; checked at the API and at the UI render path. |
Data integrity
| Risk | Control |
|---|---|
| Tampered evidence | Evidence files are content-addressable (path includes a deterministic identifier); files are stored once and referenced; modifications produce new versions, never overwrite. |
| Audit log forgery | Audit log is append-only; writes go through an interface that requires actor + scope + timestamp; no UI surface allows editing or deleting audit entries. |
| Workflow tampering | Workflow runs are executed by Temporal with deterministic step state; steps are idempotent; replay produces the same outcome. |
| Source-label spoofing | Source state is computed at the data-access layer, not from request input. A page cannot claim "live" because the URL says so; it is set by what actually loaded. |
Provider integration
| Risk | Control |
|---|---|
| Forged provider response | Provider responses go through adapter modules with payload validation. Suspicious responses are flagged for human review rather than acted on. |
| Replay attacks on submission | Every submission carries an idempotency key; providers that support idempotency are configured with it; duplicate submissions are coalesced. |
| Provider impersonation (rogue webhook) | Webhook endpoints validate signatures where the provider supports them; payloads are matched against known submissions before status updates are accepted. |
| Submission to wrong target | Submissions include client and brand context; the channel template renders are reviewed at staff signoff; production-mode submissions require enforcement.submit_live capability. |
Evidence and content
| Risk | Control |
|---|---|
| Malicious evidence files (uploaded attachments) | MIME type allow-listed; file size limited; executables rejected; images sanitized at the server. |
| XSS via evidence content | HTML analysis output is plain text or parsed structures; the UI never injects untrusted HTML; React's default escaping plus explicit dangerouslySetInnerHTML audits. |
| Stored credentials in evidence | Evidence may include text scraped from phishing pages, which can contain credential strings. Treated as case content, not as credentials; no automated propagation. |
| Bait pages targeting our screenshot capture | Screenshot capture runs in an isolated worker, with no shared cookies or state with the app. The capture worker runs in a least-privilege environment. |
Network and infrastructure
| Risk | Control |
|---|---|
| TLS downgrade | TLS 1.2+ enforced; HSTS in production; certificate management via Vercel / Render / Authentik host. |
| DNS hijack | Critical hostnames (unphish.engram.org, unphish-prod.engram.org, Authentik host) protected by registrar lock and DNSSEC where supported. |
| Compromised provider runtime | Provider adapters are limited to specific egress targets; unexpected destinations fail closed. |
| Compromised worker host | Render worker has access to env-scoped secrets only; no broader cloud credentials. Compromise is contained to the worker's blast radius. |
Supply chain
| Risk | Control |
|---|---|
| Malicious npm dependency | pnpm-lock.yaml is committed; CI validates the lockfile; dependency updates go through PR review. |
| Compromised CI | GitHub Actions secrets are scoped; deployment requires Vercel approval; production env vars are not readable by CI. |
| Build-time injection | TypeScript build errors must not be ignored; lint and typecheck must pass before merge; preview deployments are isolated from production env. |
Demo and source-label hygiene
| Risk | Control |
|---|---|
| Customer thinks demo data is live | Every production-facing surface labels its data source. Demo runs on its own host (unphish-demo.engram.org). The Demo banner is visible at all times in the demo environment. |
| Production silently falls back to fixtures | Forbidden by the source-state contract. CI tests assert unavailable is shown when live data cannot load, never fixture fallback. |
| Staging shows production data | Staging uses staging databases / branches; production data does not flow into staging. |
Trust boundaries
Trust boundaries we explicitly maintain:
- Browser ↔ App. All requests are TLS; mutations require authenticated session or scoped API key.
- App ↔ Authentik. OIDC over TLS. Admin token is held in env, rotated on Authentik
secret_rotate. - App ↔ Database. Connection over TLS; pool credentials in env; no app database role has DDL beyond migrations.
- App ↔ Worker (via Temporal Cloud). Workflows are submitted by signed signals; activity payloads are scoped per env.
- Worker ↔ Provider. TLS; per-provider credentials in env; payloads validated against expected schema.
- Customer ↔ Customer (cross-tenant). Structurally impossible without staff impersonation.
What we explicitly do not protect against (yet)
Honesty about limits:
- Compromised Authentik host. A full Authentik compromise is treated as a top-tier incident; we depend on Authentik's own controls, monitoring, and recovery procedures. We monitor for signs (failed admin token use, unexpected
secret_rotateevents, unusual sign-in patterns) but cannot fully mitigate via the app. - Compromised provider account. If a provider's customer portal is breached, attackers may see submissions we sent. We minimize stored sensitive content in submissions; we do not control the provider's posture.
- Side channels (timing, traffic analysis). Not actively mitigated; out of scope for the current threat model.
- Insider with full DB access. An attacker with direct Postgres access can read tenant data. We minimize the number of people with that access; we audit changes to access; we do not currently encrypt at the application layer beyond what the database provides.
Reporting suspected issues
Security issues should be reported to security@engram.org (or the equivalent established channel). Coordinated disclosure is welcomed; we will acknowledge promptly, assess severity, and provide remediation timelines.