PAR-183
Cross-tenant IDOR: scope by-UUID queries to the active tenant
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)updateTaskread-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
- commit32a1fa2 Harden cross-tenant label writes + add IDOR regression tests (PAR-183)ParadauxIO/tesks
- commit33832ab Scope by-UUID task queries to the active tenant (PAR-183)ParadauxIO/tesks
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:
- Label write — the
insert into issue_labelaccepted a rawlabel_idfrom the caller with no tenant check (could attach another tenant's label to your own issue). Scoped the insert tolabelrows owned by the active tenant (foreign/bogus ids are silently skipped). - 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
updateTaskwon't attach a foreign label. All 37PostgresTaskSystemTesttests 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