The read that checked the wrong organization
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").
ctx.OrgID instead of the target user's ResourceOwner.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.