Security research

The read that checked the wrong organization

Dmitry Maranik

A published advisory in Zitadel, the open-source identity platform, let any organization member who held user.read in one organization learn which authentication methods a user in any other organization on the same instance had registered — password, passkey, TOTP, hardware key, SMS, email OTP, IDP, recovery codes. No secrets leaked. What leaked was every account's MFA posture, across the tenant boundary. It is the pattern we keep reporting: a scope check that was built correctly for the write path and skipped on the sibling read.

Zitadel is multi-tenant by organization: users, roles, and the authentication methods behind them belong to an org, and an org's members are not supposed to see across that line. On 2026-09-04 the maintainers published GHSA-j344-gqv4-84ff, a Medium cross-organization authorization flaw (CWE-862), fixed in Zitadel 4.17.3. This is a walk through what it was, why the check pointed at the wrong organization, and an honest note on how we were involved.

What broke

The User Service exposes GET /v2/users/{userId}/authentication_methods (the ListAuthenticationMethodTypes call, plus its v2beta twin). It answers a simple question: which authentication method types does this user have set up? That answer is scoped data — it should only be visible to someone allowed to read that user, in that user's organization.

The endpoint did check for user.read. It just checked it against the caller's organization context, not against the organization that owns the target user. So the permission the server confirmed ("you may read users in your org") had nothing to do with the resource it then returned ("this user, who lives in a different org").

Why it matters more than "just types"

The endpoint returns method types, not secrets: no passwords, no keys, no enrollment codes, and no ability to change anything. That keeps it at Medium. But MFA posture is exactly the reconnaissance an attacker wants before going after an account. A member with the read-only ORG_OWNER_VIEWER role in one org — a deliberately low-privilege, look-but-do-not-touch grant — could sweep users in other orgs and learn which accounts have only a password and no second factor, or which rely on SMS versus a hardware key. On a shared instance with many orgs, that is a cross-tenant map of where the soft targets are.

The pattern: the write path was fixed, the read sibling wasn't

What makes this one a clean example is that Zitadel had already been down this exact hallway. The advisory itself notes the flaw is related in shape to an earlier one, GHSA-pq2q-2c6r-75c4, which fixed a cross-organization issue on a write path — issuing enrollment codes across the org boundary. The team correctly re-scoped that write. The neighboring read, which answered "what methods does this user have," kept checking the caller's org.

This is the un-retrofitted read sibling in its purest form: the isolation fix lands where the incident was, on the write, and the read a few endpoints over — same boundary, same resource, lower stakes, less scrutiny — keeps the old, wrong scope. It is why we diff a codebase's tenant check across all of its call sites, not just the one that was in the last CVE. The reads, the deletes, and the "list types" endpoints are where the boundary quietly survives a retrofit.

The fix

One change, in the one place the scope was wrong: resolve the target user's owning organization first, and check user.read against that before returning any method types. Same-organization reads by callers who legitimately hold user.read there are unchanged; instance-wide roles built to manage users across orgs are unaffected; single-org deployments never had a boundary to cross.

Our part, honestly

We reported this to Zitadel's security team and are credited as a reporter on the published advisory, alongside another credited reporter and Zitadel's own security engineers, who coordinated and shipped the fix. We found it the way we describe above — by walking the authentication-method surface and comparing where the org boundary was enforced against where it wasn't, after a prior write-path fix showed the boundary was meant to be there. Zitadel handled the report well: fast acknowledgement, a real conversation, and a published, version-pinned advisory. We are writing it up because it is a textbook instance of the pattern our research is about, and a clean public one is more useful than another abstract description.

Check your own read siblings

If you build anything multi-tenant, the question this bug asks is precise: when you check a permission, which tenant is it scoped to — the caller's, or the one that owns the resource you are about to return? Those are the same value on the happy path and different exactly when it matters. The dangerous endpoints are the quiet ones next to a recently-fixed hole: the reads and the enumerations that reuse the query but check the wrong side of the boundary.

We built Sectum AI to answer that at runtime: it stands up synthetic tenants, plants canary data, and checks whether one tenant's identity can reach another's data or metadata across your surfaces — with a signed evidence pack you can verify yourself. It is open source; point it at your own stack.

Explore the open source More disclosures

Advisory: GHSA-j344-gqv4-84ff (published 2026-09-04, Medium, CWE-862), fixed in Zitadel 4.17.3; affects 4.0.0–4.17.2 and the end-of-life 3.0.0–3.4.15. Credited as a reporter: Dmitry Maranik (Sectum AI), with a co-reporter and Zitadel Security.