Threat model
Actors
Section titled “Actors”| 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) |
Assumptions
Section titled “Assumptions”- TLS terminates in front of z4j. No plaintext HTTP in production.
- Env vars are secret. Leaking
Z4J_SECRETcompromises 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. LeakingZ4J_SESSION_SECRETcompromises 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), whereidis a random UUID, not a sequence. The writer clamps each newoccurred_atto 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.
In-scope threats
Section titled “In-scope threats”- Credential stuffing / brute force - defended by argon2id + rate limits + dummy-hash timing.
- Session theft - HttpOnly + Secure + SameSite=Lax cookies; session invalidation on password change.
- 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.
- 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_SECRETSrotation 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. - 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. - Replay attacks - WebSocket frames have monotonic seq; duplicates deduped on persist.
- SSRF via public_url -
Z4J_PUBLIC_URLis validated (no whitespace, no user:pass, http/https only). - 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.
Out of scope
Section titled “Out of scope”- 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_staterow 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 matchingschedule_change_logrow, 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.
What we have not fixed
Section titled “What we have not fixed”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.
Reporting issues
Section titled “Reporting issues”[email protected]. Do not file a public issue for undisclosed vulnerabilities. See disclosure.