Before launching connectors, we closed four ways one company could reach another's data. Why connectors open gaps like these, and how to check for them.
In a multi-tenant app, one missing check can show one customer another customer’s files. Before we launched connectors at BHyve, I found and closed four ways one company could reach another’s data. One was a SharePoint path trick that could list someone’s OneDrive.
A tenant is one customer organisation. Multi-tenant means every tenant shares the same app, servers and databases, and only your code keeps them apart. Connectors are the part of BHyve that reads from tools a company already uses, such as SharePoint, so the AI can answer from them.
I won’t go through the four holes. They’re closed, and the pattern behind holes like these is more useful than the specifics.
Why connectors are where isolation breaks
Inside your own database, you set every identifier. Each row has a tenant column, and you decide what goes in it. Connectors add identifiers that come from outside: site paths, drive IDs, folder names, mailbox addresses. They often arrive in a request, and they get handed to someone else’s API along with a stored credential.
That API checks the credential. It has no idea which of your customers is asking. If one tenant’s request ends up using another tenant’s connection, or names a path outside what that tenant connected, the other side simply serves it. Your code is the only thing deciding what each request may touch.
It also helps to know how the other system is built. In Microsoft 365, OneDrive for work or school is built on SharePoint. So a path that looks like it belongs to a company’s SharePoint can lead somewhere you didn’t expect, such as a person’s OneDrive.
That’s the usual cause of a cross-tenant leak: the app trusts an identifier the caller supplied. A tenant ID, a record ID, a folder path. Each one is a request to see something. None of them is permission.
The principle: naming is not authorization
OWASP’s multi-tenant guidance puts it in four words: naming is not authorization. Decide the tenant on the server, from the signed-in session, and check everything the request names against it. Fix it once, in the place every request passes through, rather than one endpoint at a time.
How to check a multi-tenant app
-
Take the tenant from the session. A tenant ID in a URL, header or form field can be edited. OWASP’s guidance says to treat client-supplied tenant IDs as selectors only, and to bind the tenant to a server-verified identity and its current membership. If a user belongs to several workspaces, check the one they name against the session’s memberships.
-
Pick connector credentials by the session’s tenant, then check the resource. Choose the stored connection using the tenant from the session. Then check that the site, drive or mailbox the request names belongs to that connection. A valid credential plus an unchecked identifier is a cross-tenant read.
-
Normalise paths before you check them. String checks are easy to fool.
../climbs out of a folder, and%2e%2e%2fis the same thing URL-encoded. A backslash can act as a separator. A prefix check for/sites/acmealso matches/sites/acme-west. So decode, resolve.and.., settle on one separator, then compare against the tenant’s allowed roots with a trailing separator. Reject anything odd instead of repairing it, and check the exact string you’ll send to the API. -
Prefer stored IDs to free-form paths. Keep the IDs of the sites and folders a tenant connected, and let requests choose from that list. Then there’s no path to trick.
-
Scope every query by tenant. Fetching a record by its ID without checking who is asking is called broken object level authorization (BOLA), or IDOR. It’s first in the OWASP API Security Top 10 for 2023. As a general example in SQL:
-- Risky: anyone with a document ID gets the document SELECT * FROM documents WHERE id = $1 -- Safe: the tenant comes from the session, not the request SELECT * FROM documents WHERE id = $1 AND tenant_id = $2 -
Add a backstop that fails closed. Database row-level security (RLS) makes the database itself refuse rows from other tenants, so a forgotten filter returns nothing instead of everything. OWASP adds two cautions. Run requests as a role that can’t bypass the policies. And set the tenant for every transaction, so a pooled connection doesn’t carry the previous request’s tenant.
-
Cover the places people forget. Cache keys, search and vector indexes, background jobs and stored files all need the tenant too. OWASP points out that a shared queue is not a tenant boundary on its own. A job should know which tenant it’s working for and check again.
-
Test like an attacker, with two tenants. Sign in as tenant A and try every ID, path and name that belongs to tenant B, on every endpoint, encoded and plain. Every call should fail, ideally the same way a missing record does, so the error doesn’t confirm that B’s record exists. Then check that A can still read A’s data. A check that blocks everything passes the first test and breaks the product.
-
Run those tests on every change. OWASP’s API guidance says not to deploy changes that break authorization tests. New endpoints are where holes come back.
What it changed
All four holes were closed before connectors launched. That’s the cheapest moment to find them: before any customer has connected a tool, and before a fix means telling someone their data was exposed.
The lesson
Every identifier from outside is a request, not a permission. Decide the tenant on the server, and check the rest against it.
The rest of the security work is in the case study. Sessions matter here too, because the session is where the tenant comes from: see a session security checklist. Uploaded files are another way hostile content gets in, covered in safe file uploads.
Quick answers
- What is tenant isolation?
- Tenant isolation means each customer of a shared app can only see and change its own data, even though every customer runs on the same servers and databases.
- Should a multi-tenant app take the tenant ID from the request?
- No. Take it from the signed-in session on the server. A tenant ID in a URL, header or body can be edited, so OWASP says to treat it only as a selector that is checked against the session.
- What is BOLA or IDOR?
- Broken object level authorization (BOLA), also called IDOR, is when an app fetches a record by its ID without checking that the caller may see it. It is first in the OWASP API Security Top 10 for 2023.
- How do you test for cross-tenant data leaks?
- Create two test tenants, sign in as one, and call every endpoint with the other tenant's IDs, paths and names. Every call should fail, and the same calls with your own data should still work.