A Tenancy Review Found One Customer Could Read Every Customer's Data

A Tenancy Review Found One Customer Could Read Every Customer's Data

On 21 September a review of our own marketplace API found that GET /api/v1/reputation/requests ran SELECT client_name, client_email FROM review_requests with no WHERE clause. Its handler took no tenant argument. Any caller holding a valid key received every site’s customer names and email addresses.

Four separate passes followed, between 21 September and 9 October. Each one found more than the last, and the third was created by the second — the same pattern as the fix for one security finding creating the next one. That sequence — not the first finding — is the actual argument for a review rather than a spot check.

The missing clause was a symptom, not the cause

A missing WHERE reads like carelessness, and fixing it reads like a one-line patch. It was not, and that is the part worth sitting with.

Every WordPress install authenticated with the same shared settings.api_key. That key is distributed to every customer by design — it ships in the plugin build. So the API had no way to tell one site from another. There was no identity available to filter on, which means the WHERE clause could not have been written even by someone who thought to write it.

Put plainly: every customer held a credential that read everyone’s data, and the code was behaving exactly as its authentication model allowed.

The same design surfaced a second way. reputation_settings was a single global row — CHECK id = 1 — that each site overwrote in turn, so one customer’s review URLs and email template replaced another’s. Two different defects, one root: a tenant table with no tenants in it. The isolation models worth comparing it against are the ones written down for enterprise Kubernetes multi-tenancy — namespace per tenant, or a shared plane with policy — because both of those at least start from a tenant identity the platform can see.

The fix was not the clause. It was giving identity somewhere to live, in app/tenant.py: a Wix instance header resolves to wix_<instance_id>, and an API key matching a subscription’s licence key resolves to that subscription. Once a request can say which tenant it is, filtering becomes possible — and only then is a missing WHERE a bug you can fix.

Then the second pass, which found three more

On 8 October an adversarial review demonstrated three further bypasses against the running application. Not theorised. Demonstrated, on the deployed app.

Two of the three predated the quota work entirely and were live in deployed September code. The third was introduced by the quota work itself — by the fix.

Unauthenticated tenant self-provisioning, live since 24 March. POST /license/checkout with plan=free required no authentication and no verification, and its app field was a free-form string. Free-key minting deduplicated on the pair (email, app), so varying app turned “one free tenant per address” into unlimited tenants per address — each carrying free-tier Claude and Tavily quota billed to our own keys.

Now, take that one only. The reviewer minted 6 tenants from a single email address and spent all 6. Not a proof-of-concept against a staging copy — six real tenants, six real quotas, consumed. app is now a Literal over the four real products, so one address yields at most four tenants.

A cross-tenant quota write, live since 3 March. POST /license/usage took license_key from the request body, guarded only by require_api_key — which the same change set had just widened to accept the shared key shipping in every plugin build. Any installer could drive an arbitrary customer’s usage counters.

Backup jobs were not scoped per tenant. One customer could repoint another customer’s backup URL.

Then a fourth pass, on 9 October: free signup gated, and the practice of issuing licence keys by email address stopped.

What the sequence says that the findings do not

Any one of these findings, on its own, supports a comfortable story: we found a bug, we fixed the bug. Four passes each finding more does not support that story.

Per-tenant metering is worthless if anyone can mint tenants for free, write to another tenant’s counters, or choose which product’s entitlement pays. The first pass gave the system an identity. The second pass asked what that identity was still not protecting, and found three answers. The metering work introduced one of them.

So a review is not a search for defects. It is a search for the assumption the defects are sitting on — and you cannot find an assumption by reading a diff, because the diff is written by someone who holds it.

That is also why the second pass mattered more than the first. A spot check validates the fix. An adversarial pass asks what the fix now permits.

The limit we did not close, stated as a limit

One thing stayed open, and a review that reports only what it closed is advertising, not a review.

Unlimited email addresses remain possible. Capping app to four products caps tenants per address at four; it does nothing about a caller with forty addresses. Closing that needs email verification, which is a product decision about signup friction, not a bug fix. It is recorded as open rather than quietly counted among the wins.

Two customers reading each other’s data is an incident. An ordinary free-tier abuse surface that every SaaS has is a cost of doing business. A review earns its fee partly by telling you which of the two you are looking at.

What this costs before and after a customer finds it

Our findings were found by us, in a repository we control, on an application whose customers had not yet noticed. The remediation was five commits across eighteen days.

The same findings reached by a customer are a different object entirely. client_name and client_email belonging to every site, reachable with a credential every customer already holds, is the shape of exposure that triggers a privacy assessment rather than a patch — and the assessment, the disclosure decision and the customer conversations all land before any code moves. The code change is identical. Everything around it is not.

What we need, before the next marketplace product ships, is a tenancy pass treated as a release gate rather than a response — run adversarially, on the deployed build, by someone who did not write the authentication model and therefore does not share its assumptions.


Multi-tenancy reviews, control design and the technical due diligence underneath them are part of the advisory we do through Ganda Tech Services, with infrastructure and security operations through Cloud Geeks.

Free Roadmap · 2026

Digital Transformation Roadmap 2026

A 12-month framework for Australian SMBs ready to modernise — phases, tools, and milestones.

We email a confirmation link first. No spam. Unsubscribe any time.