Finding: Any signed-in user can read another tenant's invoices
What an attacker can do A low-privilege user of any tenant can retrieve the full invoice history of any other tenant by changing a single numeric ID in a request. No special role is required; the account only needs to be able to sign in.
Structural key
- Endpoint:
GET /api/v2/invoices?organisation_id=<id>(IDs and tokens stripped in the attached trace) - Precondition: authenticated as a standard user of a tenant the tester created
- Observable: the response body contains invoice records belonging to a tenant the tester does not control
- Likely patch surface: the authorisation layer — the handler resolves
organisation_idfrom the query string instead of from the session
Reproduction
- Register two tenants, A and B, using the self-serve signup. Note tenant A's
organisation_idfrom the dashboard URL. - Sign in as a standard user of tenant B.
- Send
GET /api/v2/invoices?organisation_id=<A>with tenant B's session cookie. - The response returns HTTP 200 with tenant A's invoice line items, including amounts and customer names.
Reproduced 3/3 attempts across two days. Full request/response trace attached separately.
Impact reasoning Severity is argued from the demonstrated observable and the precondition, not the bug class. The precondition is minimal (any authenticated user), the observable is cross-tenant disclosure of financial data, and the data is real customer information in production. That combination is a High. It would be Critical if the same pattern allowed writes; testing did not demonstrate a write path.
Remediation Resolve the tenant from the authenticated session and ignore any client-supplied organisation_id. Add a regression test that asserts a tenant-B session receives 403 for a tenant-A id. The same handler family (/api/v2/invoices, /api/v2/statements, /api/v2/exports) shares the pattern and should be checked together.