CVE-2026-15630: Cross-Tenant Authorization Bypass in Casdoor IAM — Authorization vs. Action Desynchronization
| CVE | CVE-2026-15630 |
|---|---|
| CERT/CC | VU#889462 |
| Severity | Critical CVSS v3.1 base score 9.6 |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Weaknesses | CWE-863 — Incorrect Authorization • CWE-639 — Authorization Bypass Through User-Controlled Key • CWE-269 — Improper Privilege Management |
| Product | Casdoor (open-source identity and access management, casdoor/casdoor) |
| Affected | All versions through v3.115.0 (current release, published 2026-07-12) |
| Fixed in | No fix available Unpatched at time of publication |
| Reported by | Louis Sanchez — Voke Cyber |
| Coordination | CERT/CC coordinated (VU#889462). Vendor unresponsive; CERT/CC publishing to warn operators. |
No patch available
At the time of publication there is no fixed release. The flaw is live in the current version (v3.115.0) and was re-verified against master on 2026-07-12. Operators of self-hosted Casdoor should apply the interim mitigations below and treat every organization-admin account as capable of reaching every other tenant on the instance.
Summary
Casdoor's global authorization filter (routers/authz_filter.go) decides whether to allow a state-changing request by resolving the target object from the URL ?id= query parameter — a design the code documents as deliberate, to stop body-spoofing. But the affected controllers (controllers/user.go, controllers/permission.go, and roughly 25 more) ignore ?id= entirely and act on the owner and name fields of the JSON body, with no caller-authorization check at the object layer. Authorization and action look at two different objects. An attacker who is only an organization admin (IsAdmin = true) in one tenant can send a single request whose ?id= points at a resource they own and whose body names a victim in another tenant — deleting that tenant's users, planting a backdoor administrator, wiping its SSO certificates, or planting a Casbin rule that grants themselves instance-wide global admin.
Background: what Casdoor is and why the blast radius matters
Casdoor is a widely deployed open-source identity and access management platform. Organizations run it as the single sign-on backend for multi-tenant SaaS products and as a drop-in SSO provider (OIDC, OAuth, SAML, LDAP) for internal applications. A single Casdoor instance commonly holds the accounts, permissions, and federation certificates for many separate customer organizations at once.
That is exactly the property this vulnerability breaks. Casdoor's entire job is to be the authority that decides who can do what. When the authority itself can be tricked into acting on the wrong target, every tenant that trusts it is exposed at once. Each organization on the instance is supposed to be isolated — an administrator of Organization A should have no rights over Organization B. This flaw dissolves that boundary from a routine, low-privilege admin account.
Root cause: the authorization filter and the controllers disagree
Casdoor places a global authorization filter in front of its API. For any state-changing request, that filter is written to treat the ?id= value in the URL as the authoritative name of the object being acted on. The code even carries a comment explaining the reasoning: keying on the URL identifier stops an attacker from spoofing ownership by injecting a different owner value into the request body.
// routers/authz_filter.go — getObject() resolves the operated object // from the ?id= URL parameter, treated as authoritative to prevent // body-spoofing of ownership. // object == query "id" (e.g. "org-attacker/attacker-admin")
The problem is that the controllers behind the filter do not honor that contract. The affected controllers — such as those in controllers/user.go and controllers/permission.go — ignore ?id= entirely and read the target object only from the owner and name fields of the JSON request body. The database layer underneath them (object/*) performs no further check that the caller is allowed to touch that object.
// controllers/user.go — DeleteUser reads the target from the BODY, // never from ?id=. No organization or caller-authorization check. var user object.User json.Unmarshal(c.Ctx.Input.RequestBody, &user) // {"owner","name"} from body affected := object.DeleteUser(&user) // acts on body target
That disagreement is the whole vulnerability. The authorization filter approves the request because ?id= points at something the attacker legitimately owns, while the controller carries out the action against whatever the body names, in whatever organization it likes. Authorization and action are looking at two different objects.
The same body-only pattern appears across roughly 25 add- and delete- endpoints (users, permissions, invitations, groups, certificates, and more), so the flaw is systemic rather than a single stray handler.
Exploitation
Prerequisite: the attacker holds an organization-administrator account (IsAdmin = true) in any one organization on the instance. Global admin (IsGlobalAdmin) is not required, and on instances that allow self-signup this threshold is low.
The core trick is a single request that carries two different identities. The ?id= in the URL points at a resource the attacker owns, so the authorization filter approves. The JSON body names a victim in another organization, so that is what actually gets acted on.
POST /api/delete-user?id=org-attacker/attacker-admin
Cookie: <attacker session>
Content-Type: application/json
{"owner":"org-victim","name":"victim-admin"}
The server responds 200 OK and deletes org-victim/victim-admin, a user the attacker has no rights over.
Chained together, a handful of these requests take an organization admin from their own tenant to full control of another and then of the whole instance:
- Empty the victim tenant. Delete the victim organization's users, including its administrators, leaving it without an owner.
- Plant a backdoor admin. Create a new user in the victim organization with isAdmin: true — an administrator the attacker controls and can log in as.
- Escalate to global admin. Plant a Casbin permission rule in the built-in global-admin organization granting the attacker every action on every resource, handing them effective control of the entire Casdoor deployment.
- Break federation. Delete the victim organization's signing certificates to collapse its SAML or OIDC single sign-on.
None of this requires a race condition, user interaction, or any chaining beyond sending the requests. The behavior was reproduced end to end against a stock Casdoor deployment using an organization-admin account with no relationship to the victim tenant.
Impact
A non-global organization administrator can, against any other organization on the same instance:
- Delete arbitrary users, including administrators — cross-tenant denial of service and a step toward takeover
- Create arbitrary users with administrator rights — a persistent backdoor in any tenant
- Plant Casbin permission rules anywhere, including the built-in organization, escalating to instance-wide global admin
- Delete certificates, breaking SSO and SAML federation for the victim tenant
Taken together this is a complete cross-tenant compromise of the platform reachable from a single low-privilege role.
CVSS vector rationale
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
- AV:N — The Casdoor API is reachable over the network.
- AC:L — A single request. No chaining or special conditions required.
- PR:L — Any organization-admin role (IsAdmin = true); the default grant in standard signup flows. Global admin is not needed.
- UI:N — No victim interaction. The attacker drives the exploit entirely from their own session.
- S:C (Scope Changed) — A compromise scoped to the attacker's own organization affects resources owned by other organizations on the instance.
- C:H — Cross-organization read via planted Allow permission rules.
- I:H — Cross-organization user creation with attacker-chosen attributes, including isAdmin.
- A:H — Cross-organization deletion of users, admins, groups, and certificates with no recovery path.
Remediation and interim mitigations
There is no vendor fix at the time of publication. The flaw lives in how the request is handled — the authorization filter trusts the ?id= in the URL while the controllers act on the owner and name in the body — and no Casdoor setting, environment variable, or policy option makes those two agree. Until a code fix ships, operators of self-hosted Casdoor should reduce their exposure:
- Minimize organization-admin accounts. Exploitation needs one authenticated account with IsAdmin = true in any single organization. Cut org-admins to the minimum, disable any self-signup or workflow that grants organization-admin automatically, and require multi-factor authentication on every admin account you keep.
- Optionally add a reverse-proxy or WAF tripwire. The attack always shows up as a mismatch: ?id= points at the attacker's own organization while the body's owner points at someone else's. A rule on the /api/add-* and /api/delete-* endpoints can reject any request where the body owner disagrees with the organization in ?id=. Handle it carefully: exempt built-in global-admin sessions (they legitimately write across organizations), run it in log-only mode first because Casdoor's own admin UI does not always send ?id= the way the rule expects, and cover the whole add/delete family — a rule that only covers delete-user leaves the other ~24 endpoints open. Treat it as a tripwire, not a hard block.
- Monitor for the attack signature. Alert on requests to add-/delete- endpoints where the ?id= owner and the body owner disagree; on new Casbin rules appearing in the built-in organization, especially any Allow rule with * resources or actions; on a user in one organization creating admins or deleting users in a different organization; and on any sudden unexplained drop in a tenant's user count.
The correct code-level fix is for each affected controller to derive the target object from the authoritative ?id= identifier and reject any request whose body names a different object — mirroring the pattern Casdoor already uses in its update-user path — backed by a caller-authorization check at the object layer as defense in depth.
Disclosure timeline
- 2026-05-13 — Vulnerability discovered and reproduced end to end against a stock Casdoor deployment. Reported to CERT/CC (VU#889462) and MITRE the same day as independent timestamps, with vendor email to admin@casdoor.org following within 24 hours.
- 2026-05-28 — Vendor shipped fixes for nine unrelated Casdoor issues (VU#780781, CVE-2026-9090 through CVE-2026-9098 — SAML certificate/replay, MFA binding, and token exchange). This desynchronization bug was not among them.
- 2026-07-12 — Re-verified still unpatched against master (v3.115.0): the filter still keys on ?id= and the controllers still act on the body with no caller-authorization check.
- 2026-07-20 — CERT/CC assigned CVE-2026-15630, published VU#889462, and this advisory went live. The vendor could not be reached during coordination, so CERT/CC is publishing to warn operators in the absence of a patch.
References
- CERT/CC vulnerability note: VU#889462
- CVE record: CVE-2026-15630
- Casdoor project: casdoor.org • github.com/casdoor/casdoor
- Blog post (story and disclosure narrative): CVE-2026-15630: When the Authorization Check and the Action Look at Different Objects
Found and reported by Louis Sanchez, Founder & Principal Security Consultant at Voke Cyber (OSCP, OSWA, CISSP, CCSK). Prior advisories: CVE-2026-39878 (Chamilo LMS), CVE-2026-48742 (Coolify), CVE-2026-35198 (HeyForm), CVE-2026-48507 (Snipe-IT), and CVE-2026-42318 (GLPI).
We find the bugs tools miss
Authorization-vs-action desynchronization does not show up in dependency scans. It surfaces when someone reads the request pipeline and asks whether the object the system authorized is the same object it acts on. That is how we approach web application and API testing for clients across the Charlotte, NC area and nationwide.
Get a Quote