Unphish v2 Docs

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

AdversaryMotivationTypical capability
Phishing operator under enforcementAvoid 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 campaignsContinue 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 attackerIntercept traffic; pivot between services.Reads or modifies traffic between components.
Supply-chain attackerInject code via dependencies.Compromises an npm package or upstream dependency.
Provider / integration attackerTrick the platform into submitting bogus takedowns.Forges or manipulates provider responses.

Surfaces and controls

Authentication

RiskControl
Credential stuffing / brute forceAuthentik handles authentication with rate limiting, MFA enforcement at the org level, and account lockout. The app has no password endpoint at all.
Session fixationSession cookies are issued post-OIDC-callback with new identifiers; signed with HMAC; HttpOnly; Secure in production; SameSite Lax.
Token leakageOIDC tokens are not stored in the app database. Access tokens are short-lived; refresh tokens stay at Authentik.
Compromised admin tokenAUTHENTIK_ADMIN_TOKEN rotation is documented; Authentik emits secret_rotate events; Hub flags fingerprint mismatches.
Phishing of analystsBranded sign-in flow; URL is consistently unphish-staging.engram.org or unphish-prod.engram.org; staff trained to verify host before entering credentials.

Authorization

RiskControl
Cross-tenant readEvery 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 writeSame as read; mutations also write organization_id from the active session, never from request input.
Privilege escalation via role manipulationRole values are normalized server-side before persistence; database constraints reject unknown values; UI labels are display-only.
Privilege escalation via system_rolesystem_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 impersonationSupport preview is capability-gated; banner-visible; time-bounded (60-minute max); requires explicit reason; every action is double-audited.
API key scope bypassAPI keys are scoped at issuance; the API gateway enforces scope on every request and on every response field.
Brand-level access bypassReviewers with brand restrictions cannot approve other brands' cases; checked at the API and at the UI render path.

Data integrity

RiskControl
Tampered evidenceEvidence files are content-addressable (path includes a deterministic identifier); files are stored once and referenced; modifications produce new versions, never overwrite.
Audit log forgeryAudit log is append-only; writes go through an interface that requires actor + scope + timestamp; no UI surface allows editing or deleting audit entries.
Workflow tamperingWorkflow runs are executed by Temporal with deterministic step state; steps are idempotent; replay produces the same outcome.
Source-label spoofingSource 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

RiskControl
Forged provider responseProvider responses go through adapter modules with payload validation. Suspicious responses are flagged for human review rather than acted on.
Replay attacks on submissionEvery 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 targetSubmissions 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

RiskControl
Malicious evidence files (uploaded attachments)MIME type allow-listed; file size limited; executables rejected; images sanitized at the server.
XSS via evidence contentHTML 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 evidenceEvidence 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 captureScreenshot 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

RiskControl
TLS downgradeTLS 1.2+ enforced; HSTS in production; certificate management via Vercel / Render / Authentik host.
DNS hijackCritical hostnames (unphish.engram.org, unphish-prod.engram.org, Authentik host) protected by registrar lock and DNSSEC where supported.
Compromised provider runtimeProvider adapters are limited to specific egress targets; unexpected destinations fail closed.
Compromised worker hostRender worker has access to env-scoped secrets only; no broader cloud credentials. Compromise is contained to the worker's blast radius.

Supply chain

RiskControl
Malicious npm dependencypnpm-lock.yaml is committed; CI validates the lockfile; dependency updates go through PR review.
Compromised CIGitHub Actions secrets are scoped; deployment requires Vercel approval; production env vars are not readable by CI.
Build-time injectionTypeScript 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

RiskControl
Customer thinks demo data is liveEvery 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 fixturesForbidden by the source-state contract. CI tests assert unavailable is shown when live data cannot load, never fixture fallback.
Staging shows production dataStaging 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_rotate events, 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.

On this page