A customer reports seeing another company's records in your admin console. Walk me through the first hour, and then tell me what you change so this class of bug cannot happen again.
Treat it as a confirmed breach until proven otherwise: preserve logs, scope who saw what, and start the disclosure clock alongside the investigation. The cause is almost always tenant scoping living in application code, where one forgotten filter leaks everything - so move enforcement below the code, to row-level security.
What the interviewer is scoring
- Whether the candidate preserves evidence and scopes exposure before rushing to a code fix
- That disclosure obligations are started in parallel, not after the engineering work finishes
- Does the answer identify that application-layer filtering fails open, and name what failing closed looks like
- Whether row-level security, per-tenant credentials or a scoped repository layer is proposed over "add the WHERE clause"
- That the candidate asks whether other endpoints share the same defect rather than fixing only the reported one
- Whether caching, background jobs and exports are checked, since they commonly bypass request-scoped tenant context
- Does the answer include a detective control that would catch the next occurrence without a customer reporting it
Answer
Short answer
Assume a breach until you can prove otherwise. Preserve the logs, determine exactly which records were rendered to which users and for how long, and start the disclosure clock in parallel with the engineering work. Then fix the class rather than the case: a leak like this almost always means tenant scoping is enforced in application code, where any query that forgets the filter returns everything. The durable answer is to make forgetting impossible.
The first hour
Preserve before you touch. The instinct to deploy a fix immediately destroys the evidence you need to scope the exposure. Snapshot the relevant logs, capture the current code version, and note the deploy timeline before changing anything. If logs rotate hourly, this is genuinely urgent.
Scope the exposure. The questions the business will be asked are specific: which tenants' data was visible, to whom, how many records, over what window, and was any of it exported or acted upon. Reconstruct this from access logs rather than from the reproduction — the bug may have been live far longer than the customer noticed. Query volumes and response sizes on the affected endpoint often reveal the true start date.
Contain proportionately. Disabling the affected view is usually right if the leak is ongoing and the view is not load-bearing. If it is critical, a targeted patch that constrains the query is acceptable, provided you have already preserved evidence.
Start disclosure in parallel. Under GDPR the notification window is 72 hours from becoming aware, and equivalent obligations exist elsewhere and in enterprise contracts. Legal and the incident commander need to be moving while engineering investigates, because the clock does not pause for a root cause. Candidates who leave this to the end are usually revealing that they have not been through one of these.
Why it happened
The reported endpoint is rarely the interesting part. Somewhere there is a query like:
SELECT * FROM invoices WHERE status = 'overdue' ORDER BY due_date;
with the tenant predicate supplied by a service-layer helper that this particular call path did not use. Application-layer tenant filtering has a fatal property: it fails open. The consequence of forgetting the filter is not an error, it is a successful query returning more rows than it should. Nothing crashes, no test fails, and the endpoint works — for a demo tenant with one customer's data in it.
Three variants are worth checking specifically, because they bypass request-scoped context by design:
Background jobs and scheduled exports run with no user session, so whatever mechanism injects tenant scope from the request is absent. Nightly report generators are a recurring source of cross-tenant leakage.
Caches keyed without tenant id. A cache entry for invoices:overdue populated by tenant A and served to tenant B leaks data even when every query was correctly scoped. This one is particularly nasty because the code review looks clean.
Any endpoint taking an id from the client. If GET /invoices/{id} loads by primary key and checks nothing else, changing the number in the URL is the entire exploit. This is OWASP's Broken Object Level Authorization and it remains the most common serious API flaw.
Also verify where the tenant id comes from. If it is read from a request header or a body field the client can set, the "isolation" was decorative — it must be derived server-side from the authenticated session or token claims, never from client-supplied input.
Fixing the class, not the case
Adding the missing WHERE clause fixes one query and leaves the mechanism intact. The question to answer is: what makes the next engineer unable to make this mistake?
Row-level security pushes enforcement into the database, below all application code:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);
The connection sets app.tenant_id once per request, and every query on that connection is filtered whether or not the developer remembered. A forgotten predicate now returns zero rows rather than everyone's — it fails closed, which is the property you are buying. The operational costs are real and worth stating: policies must be applied to every new table, the session variable must be set reliably on pooled connections, migrations and admin tooling need a deliberate bypass path, and the planner's handling of policies can affect query plans.
Per-tenant credentials or schemas achieve the same by connecting as a role that can only see one tenant's rows. Stronger isolation, higher operational cost, and it scales poorly past a few hundred tenants.
A scoped data access layer is the pragmatic middle: no code may construct a raw query, only a repository that takes tenant scope as a required constructor argument, with a lint rule banning direct query builder access. Weaker than the database enforcing it, but achievable in an afternoon rather than a quarter, and it converts the failure from silent to compile-time.
Detecting the next one without a customer
The reason this ran undetected is that nothing was watching. Two controls change that.
An automated test that runs every endpoint twice, once as tenant A and once as tenant B, and asserts the responses share no record ids. Seeded with two tenants' data in CI, this catches the whole class mechanically and fails the build rather than the customer.
A runtime assertion that samples responses and verifies every returned record carries the requesting tenant's id, alerting on any that do not. This catches the paths tests miss — background jobs, cache hits, and endpoints added after the test suite was written.
Both are cheap relative to a disclosure. Proposing them unprompted is usually what distinguishes an answer about fixing a bug from an answer about owning a system where this bug is possible.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- The tenant id comes from a request header the client controls. What is wrong with that, and what do you use instead?
- How would row-level security have prevented this, and what does it cost you operationally?
- A background job runs with no user context. How does it get a tenant scope?
- Your cache key omits tenant id. What happens, and how would you find it?
- How do you test for this so it fails in CI rather than in production?
Related questions
- How do you isolate tenants in a shared vector index?hardAlso on multi-tenancy and tenant-isolation6 min
- Tenants share one cluster and the largest is a thousand times the smallest. How do you place them?hardAlso on multi-tenancy and tenant-isolation5 min
- Would you use iptables or eBPF for network policy enforcement in a massive multi-tenant Kubernetes cluster, and what are the operational trade-offs?hardAlso on security and multi-tenancy2 min
- One tenant bursts to ten times their normal traffic and every other customer's latency doubles. Your global rate limit was never hit. How would you design for fairness instead?hardAlso on multi-tenancy5 min