Skip to content

Threat Model

This section covers realistic threats to the platform and the controls that address them, grounded in the actual implementation. It is not an exhaustive security audit — it documents the threats the design explicitly defends against.

Threat landscape

The system's attack surface:

  • Browser sessions (agents) — the largest user population, clearance 1–5.
  • The bot (BotGhost) — holds the API key, an integration credential.
  • Public/integration entry points — OAuth endpoints, generators, the liveness route, ER:LC lookup.
  • External data — report content, evidence URLs, Discord profiles, ER:LC player data.
  • The database — Supabase (PostgREST + Realtime), grants, RLS, functions.
  • Infrastructure — Render (API), Cloudflare (Pages/Workers), secrets.

Unauthorized agent privilege escalation

Scenario: an agent raises their own rank/clearance, or gains admin access.

Controls:

  • rpc_admin_agent_update blocks self-modification (SELF_PROMOTION_BLOCKED) — enforced in the API and the database.
  • Clearance changes require Director rank (live row, not clearance level) and can never target the requester.
  • Rank changes above Head Investigator require Director rank; all rank changes are subject to the hierarchy guard (only at/below your own rank).
  • The admin allowlist gates System Administration on top of clearance; access management is Director-only and audited.
  • The bot cannot set agent_rank through any path (034); its onboarding writes are whitelisted and audited under the system identity.
  • A demoted Director loses Director capabilities immediately (rank read live per request).

Residual: a Director is by definition trusted with rank/clearance powers; compromise of a Director account is covered by credential protection and the audit trail (every change is attributable).

Unauthorized report modification

Scenario: an agent edits, deletes, or re-flags a case they have no right to touch.

Controls:

  • Assigned-agent gate on every per-report mutation (exact identifier match; supervisors bypass by rank).
  • Bot-only patchable fields (is_supervisor, notes, reason) rejected for sessions — a clearance-2 agent cannot de-escalate a supervisor case.
  • Supervisor reports are redacted (restricted placeholders, zeroed counts) for sub-Senior agents at every read surface.
  • Deletion requires clearance 5 and a reason, and writes the forensic audit in the deletion transaction.
  • Status transitions are enum-guarded at the API and re-checked under the row lock in rpc_report_action.

Audit tampering

Scenario: an attacker rewrites or deletes audit/event records, or performs a mutation without a record.

Controls:

  • Append-only grants + triggers on events, admin_audit_log, admin_health_history; no role (including service_role) can UPDATE/DELETE.
  • RLS deny-all on the streams.
  • Destructive operations write their audit row in the same transaction.
  • Actor identity is server-derived (see Audit integrity).

Race conditions

Scenario: two agents (or the bot and an agent) perform the same action concurrently; the loser's action corrupts state or double-applies.

Controls:

  • Report actions: SELECT … FOR UPDATE on the report row + status re-check; loser gets 409.
  • Queue pickups: FOR UPDATE SKIP LOCKED; two bot ticks cannot claim the same action.
  • Queue completion/requeue: locked + status guard; second completion → 409.
  • Punishment revocation: locked + terminal-status guard.
  • Training: idempotent client actions via the (session_id, client_id) unique index; the API is the sole training writer.
  • Claim: ALREADY_CLAIMED guard.

Malicious URLs

Scenario: a stored javascript:/data: URL executes in an agent's browser (stored XSS).

Controls:

  • Server-side http(s)-only validation on evidence writes.
  • Client-side safeHttpUrl/safeImageUrl re-validation at every sink (evidence, lightbox, profile images, navigation).
  • No raw HTML embedding of server content.

Stored XSS

Scenario: script content embedded in report/note/evidence text executes.

Controls:

  • React escapes text interpolation; no dangerouslySetInnerHTML for server content.
  • URL sinks are scheme-validated (the vector XSS travels through).
  • Identifier scrubbing replaces known user IDs with display names before rendering, reducing the injection payload surface.

Malformed input

Scenario: oversized bodies, megabyte keys, filter-injection strings, or invalid enum values consume resources or bypass checks.

Controls:

  • 1 MiB body cap (413).
  • Report-ID bounds + character whitelist.
  • Search-length caps; PostgREST or= metacharacter stripping on admin search.
  • Enum/whitelist validation at the API and inside the RPCs.
  • Pagination bounds on every list endpoint.

Unauthorized database access

Scenario: the browser (or any external party) reads or writes data through PostgREST/Realtime directly, bypassing the API.

Controls:

  • Browser receives only the anon key; RLS deny-all + revoked grants on every production table make it useless for data access.
  • RPC EXECUTE is service_role-only (016 lockdown).
  • Realtime tokens are short-lived, identity-scoped, and authorized by RLS membership policies.
  • The service-role key exists only on the API host, never in the browser bundle or build environment.

Accidental destructive operations

Scenario: a legitimate operator deletes a case or changes a record by mistake (or a bug triggers it).

Controls:

  • Reasons required for every destructive/admin mutation.
  • Destructive RPCs capture pre-images; deletion rolls back if the audit write fails.
  • Punishments cannot be deleted at all (no DELETE grant, trigger block).
  • Confirmation flows in the frontend for high-impact actions (AdminConfirmDialog, reason modals).
  • Report deletion is clearance-5 only.

Leaked credentials

Scenario: the API key, session secret, service-role key, or Discord client secret leaks (logs, bundle, repo, misconfigured env).

Controls:

  • Secrets are never logged (error paths log messages, not credentials).
  • The browser bundle contains only VITE_DPS_API_BASE_URL and the public anon key.
  • Environment-bound sessions and keys: a demo credential cannot authenticate against production and vice versa.
  • Repository hygiene: secret-bearing files (db.url, .env*) are ignored and never committed; this documentation contains no secret values.
  • The API key's blast radius is bounded: it cannot touch training, punishment, or admin routes.

Residual: leaked credentials require rotation at the platform level; the fail-fast boot (missing production config) and environment isolation limit blast radius but cannot undo an already-exfiltrated secret.

Non-threats (deliberate design decisions)

  • ER:LC relay tokens live only on the API host and the relay machine; routes never see them.
  • View tracking endpoints are no-ops, so there is no view-flood vector against the audit log.
  • The demo environment is intentionally weaker (allowlisted real IDs map to fictional agents) and is fully isolated from production data and credentials.