Tenant isolation bypass in report export endpoint
Summary
The report export endpoint accepts a workspace identifier from the client and returns the corresponding export without verifying that the authenticated user belongs to that workspace. Any authenticated user of any tenant can retrieve exported reports belonging to any other tenant by substituting the identifier.
Affected component
GET /api/v2/workspaces/{workspace_id}/exports/{export_id} — export service, authorization middleware not applied to this route
Business impact
A customer on the lowest-priced plan can read the exported analytics of every other customer on the platform, including any commercially sensitive data those reports contain. This is a confidentiality breach across the tenancy boundary, likely to be reportable under data protection obligations, and likely to be material in enterprise contracts that warrant tenant separation.
Exploitation requires nothing beyond a valid account on any tenant and knowledge of a workspace identifier. Identifiers are sequential and are disclosed in application responses, so enumeration is straightforward.
Severity reasoning
Rated Critical: exploitable by any authenticated user with no additional privilege, requires no user interaction from the victim, is reliably reproducible, and results in unauthorized access to other tenants\u2019 data. Cross-tenant confidentiality failure in a multi-tenant product is the highest-consequence class of finding for this architecture.
Reproduction steps
- Authenticate as analyst@tenant-b.example.com (Analyst role, Workspace 4102).
- Request GET /api/v2/workspaces/4102/exports/ and note the response format and an export identifier.
- Substitute a workspace identifier belonging to another tenant, for example 3871, retaining the same session token.
- Issue GET /api/v2/workspaces/3871/exports/88214.
- Observe that the export is returned in full, with a 200 response, despite the account having no membership of workspace 3871.
Evidence
GET /api/v2/workspaces/3871/exports/88214 HTTP/1.1
Host: api.northwind-analytics.example
Authorization: Bearer eyJ0…<tenant-b analyst token, truncated>
HTTP/1.1 200 OK
Content-Type: application/json
{
"export_id": 88214,
"workspace_id": 3871,
"workspace_name": "Acme Industrial (example)",
"generated_at": "2026-03-04T09:12:44Z",
"rows": 18422,
"download_url": "https://exports.northwind-analytics.example/…"
} Token and URL truncated. In the delivered report, evidence includes the full request and response pair, timestamps, and screenshots where relevant.
Remediation
Enforce workspace membership server-side on this route rather than trusting the client supplied identifier. The authorization middleware applied to the other routes in this service performs the correct check; this route bypasses it.
- Apply the existing workspace membership check to the export routes, and derive the permitted workspace set from the session rather than the request path.
- Audit every route in the export service for the same omission, since the pattern suggests the middleware is applied per route rather than by default.
- Change the default so authorization is required unless a route explicitly opts out, which converts this class of mistake from silent to visible.
- Consider non-sequential identifiers as defence in depth. This reduces enumeration but is not a substitute for the access check.
- Review access logs for prior exploitation, and assess notification obligations if any is found.
Retest result
Fixed and verified Membership is now enforced from the session context. Requests for a non-member workspace return 403, and the same substitution across all export routes was retested and rejected. Sequential identifiers remain in use; this was raised separately as an observation.