Skip to content

Threat model

Actor Capability Trust
External attacker Internet traffic only Untrusted
Agent host (your app) Holds an agent token; can emit any events Partially trusted
Dashboard user Has a role on one or more projects Role-scoped trust
Brain operator Full DB + env access Fully trusted
Postgres admin Direct SQL Fully trusted (defeats both in-database boundaries: audit chain and schedule control)
  • TLS terminates in front of z4j. No plaintext HTTP in production.
  • Env vars are secret. Leaking Z4J_SECRET compromises frame signing, agent-token and API-key hashing, invitation and setup tokens, and the AES-GCM key that encrypts stored TOTP secrets. Treat it as compromise of the agent transport, agent/API credentials, and MFA enrollments. Leaking Z4J_SESSION_SECRET compromises user sessions. The audit-log row chain is signed with a separate key, Z4J_AUDIT_CHAIN_SECRET, which is required outside development; keep it somewhere the database operator cannot read, since sharing it with the database would hand one party both halves.
  • Postgres is on a private network. Direct access is the operator's responsibility.
  • Audit rows are ordered by (occurred_at, id), where id is a random UUID, not a sequence. The writer clamps each new occurred_at to at least one microsecond after the authenticated head when the wall clock is not ahead. A backwards clock jump therefore cannot reorder the chain, but stored audit timestamps can move ahead of real time until the clock catches up.
  1. Credential stuffing / brute force - defended by argon2id + rate limits + dummy-hash timing.
  2. Session theft - HttpOnly + Secure + SameSite=Lax cookies; session invalidation on password change.
  3. Prompt injection via events - z4j does not evaluate event payloads as code or instructions. Task names, exceptions, and tracebacks can be rendered into in-app and external notifications. Treat notification destinations as part of the data-exposure boundary.
  4. Token enumeration - agent tokens and API keys are stored only as HMAC-SHA256 digests and resolved by digest lookup. The dummy Argon2 hash is used only for an unknown email on the email-and-password login path; no password or dummy hash is involved in token authentication. During a Z4J_PREVIOUS_SECRETS rotation window, rejecting an unknown token performs one lookup per accepted secret and can take measurably longer than accepting a token signed under the current secret.
  5. Agent impersonation - tokens are per-agent. Revocation is enforced by a durable tombstone check at authenticated work boundaries; socket kicks only accelerate disconnect. (project_id, name) uniqueness prevents duplicate-register races.
  6. Replay attacks - WebSocket frames have monotonic seq; duplicates deduped on persist.
  7. SSRF via public_url - Z4J_PUBLIC_URL is validated (no whitespace, no user:pass, http/https only).
  8. Audit tampering by anything speaking through z4j - the HMAC chain plus the separately authenticated head and row count detect it. Tampering by a role that can write the database tables directly is out of scope, and is not detected: see below.
  • Direct write access to the database, whether from physical access, stolen credentials, a shared reporting role, or a restored backup. It defeats the in-database boundaries without breaking any cryptography. The two boundaries are not equally exposed, and the difference matters if you run the packaged SQLite path. The audit-log weakness applies to both engines, because it needs no session setting and no database function, only a row restore and some deletes. The schedule-guard weakness is PostgreSQL-specific: on SQLite the guard is a connection-local function the brain installs on its own connection, so a raw client's trigger errors out and fails closed. For the audit log, such a role can delete recent rows and put back the audit_chain_state row as it stood before them; the brain signed that copy when it was current, so it still authenticates, and verification reports the shortened history as clean. For schedules on PostgreSQL, the triggers authorise a change by reading a session setting the client sets itself and by checking for a matching schedule_change_log row, and inserts into that log are unguarded, so the writer can supply its own evidence. Neither path involves a secret it cannot read. Treat the database credentials as equivalent to full control of z4j's integrity guarantees. The one check that survives is verifying against a head exported somewhere the database role cannot rewrite: see HMAC audit chain.
  • SSO / OAuth2 - not shipped. Forwarding auth through an SSO proxy in front of the brain is the supported path. (Multi-factor authentication IS shipped, and belongs in-scope: TOTP plus single-use recovery codes, trusted devices, and opt-in org-wide enforcement. See MFA.)
  • Compliance certifications (SOC 2 / HIPAA / ISO 27001) - z4j is not certified. The audit log export plus Postgres backups provide raw evidence; operators own policies, controls, and external audit.
  • Browser fingerprinting / anti-automation - we're not that kind of product.

One weakness is open by our own choice rather than by oversight: the in-database boundaries above do not hold against a role that can write their tables. This page and the HMAC audit-chain page are the current public record. The root SECURITY.md shipped from this source tree records how we rate it, when it first shipped, the mitigation available today, and what has to be demonstrably true before it comes off the list. The standalone repository policy is sourced from packages/z4j/SECURITY.md and carries the same limitation. A split-repository checkout that lacks that entry predates the corrected policy; use this page or the root policy instead.

[email protected]. Do not file a public issue for undisclosed vulnerabilities. See disclosure.