Summary
Multiple read endpoints resolve an object by numeric id without filtering by the caller's tenant, leaking cross-tenant data to anyone who guesses/enumerates ids. Surfaced during the Issues+Tickets merge (#13) tenant-scoping work (PRs #235/#236 fixed issues, projects, milestones).
Confirmed live instance (High)
GET /api/v1/organizations/<org_id>/tree-stats — apps/api/modules/infrastructure/routes/organization_tree.py:53 does root_org = db.organizations[org_id] completely unscoped, then returns for the entire descendant org tree: organization_name, total_projects, total_milestones, total_issues, total_entities, plus status/priority/type breakdowns. Any authenticated tenant can read another tenant's org name + counts by iterating org_id.
Fix pattern (already established)
Resolve tenant-scoped: db((db.organizations.id==org_id) & (db.organizations.tenant_id==_tenant_id())).select().first() → 404 if not found (treat cross-tenant as not-found), matching the pattern now used in issues.py/projects.py/milestones.py.
Broader ask — full tenant-scoping route audit
This is a class of bug, not a one-off. projects/milestones were entirely unscoped until #236. A systematic sweep of every route that does db.<table>[id] / unscoped db(db.<table>.id==id) is warranted, plus a repeatable check (grep gate) to prevent regressions. Candidate hot spots noted so far: organization_tree.py, graph.py (verify), and any module route not yet audited.
Acceptance
tree-stats (and any sibling org_tree/graph reads) tenant-scoped with a regression test asserting a tenant-1 caller gets 404 on a tenant-2 org.
- A documented audit pass over all module routes for the unscoped-lookup pattern.
Refs: #13, #235, #236. Regression test required before close (per testing standards).
Summary
Multiple read endpoints resolve an object by numeric id without filtering by the caller's tenant, leaking cross-tenant data to anyone who guesses/enumerates ids. Surfaced during the Issues+Tickets merge (#13) tenant-scoping work (PRs #235/#236 fixed
issues,projects,milestones).Confirmed live instance (High)
GET /api/v1/organizations/<org_id>/tree-stats—apps/api/modules/infrastructure/routes/organization_tree.py:53doesroot_org = db.organizations[org_id]completely unscoped, then returns for the entire descendant org tree:organization_name,total_projects,total_milestones,total_issues,total_entities, plus status/priority/type breakdowns. Any authenticated tenant can read another tenant's org name + counts by iteratingorg_id.Fix pattern (already established)
Resolve tenant-scoped:
db((db.organizations.id==org_id) & (db.organizations.tenant_id==_tenant_id())).select().first()→ 404 if not found (treat cross-tenant as not-found), matching the pattern now used inissues.py/projects.py/milestones.py.Broader ask — full tenant-scoping route audit
This is a class of bug, not a one-off.
projects/milestoneswere entirely unscoped until #236. A systematic sweep of every route that doesdb.<table>[id]/ unscopeddb(db.<table>.id==id)is warranted, plus a repeatable check (grep gate) to prevent regressions. Candidate hot spots noted so far:organization_tree.py,graph.py(verify), and any module route not yet audited.Acceptance
tree-stats(and any sibling org_tree/graph reads) tenant-scoped with a regression test asserting a tenant-1 caller gets 404 on a tenant-2 org.Refs: #13, #235, #236. Regression test required before close (per testing standards).