Skip to content

fix(security): enforce workspace scoping and role checks on job/connection/webhook routes - #575

Merged
jonwiggins merged 1 commit into
mainfrom
fix/workflow-and-route-authz
Aug 8, 2026
Merged

fix(security): enforce workspace scoping and role checks on job/connection/webhook routes#575
jonwiggins merged 1 commit into
mainfrom
fix/workflow-and-route-authz

Conversation

@jonwiggins

Copy link
Copy Markdown
Owner

SECURITY

Fixes three linked cross-tenant authorization gaps confirmed by an internal security review. No working exploit payloads are included.

A) CRITICAL — cross-tenant Job (workflow) hijack

The id-addressed handlers in routes/workflows.ts resolved the workflow/run without any workspace check, so any authenticated tenant could edit, clone, run, or delete another tenant's Job and read its runs/logs by guessing/enumerating a UUID:

  • PATCH /api/jobs/:id, POST /api/jobs/:id/clone, DELETE /api/jobs/:id, POST /api/jobs/:id/runs
  • GET /api/workflow-runs/:id, POST /api/workflow-runs/:id/retry, POST /api/workflow-runs/:id/cancel, GET /api/workflow-runs/:id/logs

Fix: added two resolver helpers mirroring the routes/tasks.ts pattern — requireWorkflowInWorkspace() and requireWorkflowRunInWorkspace() (runs carry no workspace column, so ownership is derived from the parent workflow). Every id handler now resolves through them and returns 404 (not 403) for missing or foreign resources, before any mutation runs.

B) MEDIUM — missing RBAC on mutating routes

Mutating routes across workflows.ts, tasks-unified.ts, and webhooks.ts had no requireRole, so a viewer could create/run/delete. Matched the tasks.ts convention by adding preHandler: [requireRole("member")] to:

  • workflows: POST /api/jobs, PATCH/DELETE /api/jobs/:id, POST /api/jobs/:id/clone, POST /api/jobs/:id/runs, POST /api/workflow-runs/:id/retry|cancel
  • tasks-unified: POST /api/tasks/:id/runs, POST/PATCH/DELETE /api/tasks/:id/triggers
  • webhooks: POST /api/webhooks, PATCH/DELETE /api/webhooks/:id, POST /api/webhooks/:id/test

(The unified task/trigger routes already scope the id via resolveAnyTaskById(id, workspaceId); that stays intact — this only adds the role gate. A test locks in that the resolver is still called with the caller's workspace.)

C) MEDIUM — connection-assignment IDOR

The connection-assignment routes in routes/connections.ts called connectionService.getConnection(id) (or nothing at all, for the flat routes) with no workspace check, allowing cross-tenant credential-binding tamper:

  • GET/POST /api/connections/:id/assignments
  • PATCH/DELETE /api/connection-assignments/:id (these took only the assignment id — a pure IDOR)

Fix: added a requireOwnedConnection(req, reply, connectionId) guard (404 on missing/foreign, mirroring the sibling connection routes) and a read-only connectionService.getAssignment(id) so the flat routes can resolve the owning connection before acting. Assignment mutations also now require member.

Notes

  • Foreign resources return 404 everywhere (indistinguishable from missing).
  • Auth-disabled local dev is preserved — requireRole and the workspace checks already short-circuit via isAuthDisabled() / null workspaceId.
  • Unscoped legacy rows (null workspaceId) remain accessible, consistent with the existing tasks.ts/webhooks.ts handlers.
  • No changes to workflow-service.ts business logic (other open PRs touch it); only read-only getters are consumed. persistent-agents.ts / persistent-agent-service.ts untouched.

Tests

Extended the existing route test files (workflows.test.ts, connections.test.ts, webhooks.test.ts, tasks-unified.test.ts): assert 404 for cross-workspace id access on the (A)/(C) routes and 403 for a viewer on the (B) mutations, verifying no service mutation is reached in either case.

  • apps/api unit suite: 2202 passed
  • apps/api typecheck (tsc --noEmit): clean
  • workspace pnpm turbo typecheck: 12/12 pass
  • pnpm format:check: clean

Found via internal security review.

@jonwiggins
jonwiggins merged commit 46f4b60 into main Aug 8, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant