Skip to content

Threat model

ThreatMitigation
LLM emits a block type that doesn’t existSkipped with block_type_not_registered; reason text lists available types.
LLM tries to set a hidden() param (e.g. accessToken)Caught at op application; never reaches the connector.
LLM creates a cyclecycle_introduced skip; whole patch still applies, just the bad edge is dropped.
LLM produces a workflow that calls a nonexistent connector at run timeConnector lookup at execution; run fails with attributable error in step_attempts.
Prototype pollution via JSON.parse of a patch bodyDefended against — see engine adversarial tests.
ThreatMitigation
Tenant A reads tenant B’s workflowReturns 404 not_found, not 403. Cross-org reads are structurally impossible (every store query includes organization_id = $).
Tenant A binds a schedule to tenant B’s workflowSchedule registration verifies the workflow is in the caller’s org.
Tenant A registers a webhook route pointing at tenant B’s workflowWebhook routes are programmatic (boot code), not HTTP-mutable. There is no API surface to do this.
Tenant A starves tenant B with a request floodRate limit is per-(org, principal). One tenant’s bucket cannot deplete another’s.
Compromised API keyRevoke via POST /api/auth/api-key/delete. No caching layer keeps stale keys alive.
ThreatMitigation
Anonymous request to a protected route401 unauthorized. Fail-closed.
Empty apikey table = silent open API401 — the database is the source of truth, an empty table authorizes nothing.
User has no orgs but has a session401 no_active_organization.
Origin-less /api/auth/* request403 MISSING_OR_NULL_ORIGIN — better-auth’s CSRF gate.
Brute force against email+passwordbetter-auth ships rate limiting on auth routes. Tune via rateLimit in auth options.
Cold-start route (/api/bootstrap) misuseOnly opens if THODARE_BOOTSTRAP=1 AND user table is empty AND the signed token matches. Triple-gated.
ThreatMitigation
Mid-run worker crashopenworkflow’s step cache; replay re-executes only un-cached steps.
Network blip during stepopenworkflow’s per-step retry policy.
Mid-run patch to the workflowPin-at-run-start: the run uses snapshotted JSON.
Operator deletes a workflow with active runsSoft delete; in-flight runs keep their pinned JSON.
Two ticks claim the same schedulePersistent claim via SELECT … FOR UPDATE + last_fired_at advance. Exactly-once across N tickers.
  • TLS termination. Run behind your own gateway / load balancer.
  • Email deliverability. better-auth’s email hooks need a working email provider you wire up.
  • Authn for webhook senders. Per-route HMAC verification is your responsibility (see Register a webhook route).
  • Database backups + DR. Postgres operational concerns are yours.
  • Connector-side auth. A slack connector that holds a Slack token is responsible for its own secret hygiene. Hidden params are the scaffold; the actual storage is your call.
  • Code execution sandboxing. There is no code_execute block in the default catalog. If you ship one, isolate it (isolated-vm, Cloudflare Workers, Wasm) — Thodare doesn’t provide a sandbox.

@thodare/engine includes 45+ adversarial tests:

  • prototype pollution (object literal AND JSON.parse paths)
  • 50-op batches with mixed validity
  • 3-block cycles, self-loops, downstream-reference errors
  • disabled-block references, tools that throw non-Error / return undefined
  • durable cancel mid-execution
  • 100-block fan-out in <6s
  • 20 concurrent in-memory runs
  • replay-determinism for Date.now() (cached step calls return identical values)

Full enumeration in packages/engine/THREAT-MODEL.md.

Security reports: security@thodare.dev (or via GitHub Security Advisories). Don’t open public issues for security bugs.