Paradaux

PAR-183

0

Cross-tenant IDOR: scope by-UUID queries to the active tenant

DoneUrgentUnassignedTesksBug

Child of PAR-182. Highest-priority security fix.

Several by-UUID paths in PostgresTaskSystem (tesks/src/main/java/io/paradaux/tesks/tasks/store/PostgresTaskSystem.java) act on a raw issue UUID with no tenant_id predicate, so anyone who learns/guesses another workspace's issue UUID can read or mutate it. Today it's only safe because UUIDs leak solely through tenant-scoped lists — no defense-in-depth.

Fix: add and tenant_id = ?::uuid (from TenantContext) to every by-id read/write, matching the pattern reorderTask/moveTask already use.

Paths to fix (line numbers approximate):

  • taskById (~285)
  • getTaskDetail (~307)
  • listComments (~336)
  • listActivity (~345)
  • updateTask read-current + update (~425, ~467)
  • deleteTask (~540)
  • label writes (~472-477)

Acceptance: a request bound to tenant A cannot read/mutate/delete an issue, comment, activity row, or labels belonging to tenant B even with a valid UUID. Add a regression test asserting cross-tenant by-id access returns not-found.

Resources

Comments

tesks · Jun 15, 2026, 7:43 PM

Code-complete (pending commit). Added and tenant_id = ?::uuid (from TenantContext) to every by-UUID path in PostgresTaskSystem: taskById, getTaskDetail, listComments (via join to issue), listActivity (via join), updateTask (current-read + the update statement), and deleteTask. Cross-tenant by-id access now returns not-found / no-op, matching the reorderTask/moveTask pattern.

Regression test added: PostgresTaskSystemTest.byIdPathsAreTenantScoped — asserts a second tenant holding a known issue UUID cannot read detail/comments/activity, cannot update or delete it, and the owning tenant's data is untouched. Full suite green (93 tests).

tesks · Jun 16, 2026, 8:00 PM

Completed on develop (32a1fa2).

On review, the core IDOR was already closed: taskById, getTaskDetail, listComments, listActivity, deleteTask are all tenant_id-scoped, and updateTask does a tenant-scoped current-read and early-returns (if (cur == null) return false;) so its label/event writes are unreachable for an out-of-tenant issue.

Two checklist items remained, now done:

  1. Label write — the insert into issue_label accepted a raw label_id from the caller with no tenant check (could attach another tenant's label to your own issue). Scoped the insert to label rows owned by the active tenant (foreign/bogus ids are silently skipped).
  2. Regression test (the acceptance criterion) — added two through-the-DAL tests: a tenant-A caller cannot read/mutate/delete a tenant-B issue / comments / activity by UUID (and B's data stays intact), and updateTask won't attach a foreign label. All 37 PostgresTaskSystemTest tests pass.

Acceptance met.

Activity

  • tesks changed status to Status → Done
  • tesks commented
  • ParadauxIO linked a commit — Commit 32a1fa2 — Harden cross-tenant label writes + add IDOR regression tests (PAR-183)
  • ParadauxIO linked a commit — Commit 33832ab — Scope by-UUID task queries to the active tenant (PAR-183)
  • tesks changed status to Status → Pending Release
  • tesks commented
  • tesks created the issue