One account reaching another account’s data is the most ordinary bug in multi-tenant software, and it keeps shipping because the check that prevents it is written in the wrong layer.
It also happens to be the bug class that survives every wave of automation, and there is now decent public evidence for why.
What machines find, and what they miss
HackerOne’s 2025 researcher signals report covers the first year in which automated “hackbots” submitted at meaningful volume. More than 1,100 hackbot submissions came in and nearly half were valid, a genuinely good hit rate. The interesting number is what they found.
- 78%
- Of valid automated hackbot findings were cross-site scripting, a single pattern-matchable bug class
- 73%
- Of programmes saw a drop in valid submissions as surface-level bugs ran out
- Rising
- Access control, IDOR variations and misconfiguration findings, as XSS declines from its 2024 peak
- Highest
- Payouts go to access-control and logic flaws, not surface findings
Four out of five machine-found bugs were one bug class, and it was the class you can recognise from the shape of a response. Tenant isolation is not in that list, and it never will be, because finding it requires knowing which records should have been invisible.
A scanner has one account. Tenant isolation needs two, plus the knowledge of which identifier means what.
How it usually looks
The main application is careful. Every controller for the primary resource checks that the requested record belongs to the caller’s tenant, and it does so correctly. Then a second entry point is added six months later: an export, a scheduled report, a webhook replay, an admin-facing search. The check does not come with it.
POST /v2/invoices/export HTTP/1.1
Authorization: Bearer <tenant-a-token>
Content-Type: application/json
{ "tenant_id": "tenant-b", "format": "csv" }The token is honest. The session is valid. The application simply trusted a tenant identifier that arrived from the client, because on this one endpoint nobody scoped the query. There is no malformed input for a scanner to notice. The request is well-formed and the response is a valid CSV.
The three doors
- Export and reporting endpoints, often written by a different team against the database directly.
- Webhook and integration replay, where the payload carries an identifier the application dereferences without re-checking ownership.
- Internal admin tooling exposed on the same host, reachable with an ordinary user token because the only control is an unlinked URL.
The fix that actually holds
Stop enforcing tenancy in controllers. Enforce it in the data access layer, so every query is scoped by the caller’s tenant before a developer gets a chance to forget. Row-level security in the database, a repository that will not construct an unscoped query, a base query object that requires a tenant. Any of these work. Another guard clause does not, because the next endpoint will be written by someone who did not read this one.
- Make the unscoped query impossible to express, rather than merely discouraged in review.
- Never accept a tenant identifier from the client. Derive it from the session on every request.
- Use opaque, non-sequential object identifiers so a missing check is not trivially walkable.
- Add a regression test with two tenants that asserts a 404, and run it against every new endpoint.
A control enforced in ten places is enforced in nine places by this time next year.
When we report this, the report includes the exact request, the response containing the other tenant’s data, and the specific place in your stack where the scope should live. Not a severity rating and a link to OWASP.