CVE-2026-53515: @better-auth/sso Broken Object-Level Authorization on SSO Provider Registration
Any regular member of a Better Auth organization could register an attacker-controlled SSO identity provider for that organization, potentially letting the attacker inject arbitrary users into the org
The problem
The `POST /sso/register` endpoint in `@better-auth/sso` checked that the caller was a member of the target organization but never checked their role. A user with the default `member` role could supply any `organizationId` and attach an OIDC or SAML provider they control.
All other SSO provider management endpoints (get, update, delete) already required `owner` or `admin`. The create path was the only one that skipped the role gate, creating a privilege boundary mismatch on the same resource. Once a malicious provider is registered, subsequent calls to `/sso/callback/{providerId}` can add attacker-controlled IdP users into the target organization at the configured default role, which may be `admin`.
Proof of concept
A working proof-of-concept for CVE-2026-53515 in @better-auth/sso, with the exact payload below.
POST /api/auth/sso/register HTTP/1.1
Host: target.example.com
Content-Type: application/json
Cookie: better-auth.session_token=<regular_member_session>
{
"providerId": "attacker-idp",
"issuer": "https://attacker.example.com",
"domain": "victim-org.com",
"organizationId": "<victim_org_id>",
"oidcConfig": {
"clientId": "attacker-client",
"clientSecret": "attacker-secret",
"discoveryEndpoint": "https://attacker.example.com/.well-known/openid-configuration"
}
}The root cause is CWE-285 (Improper Authorization) combined with CWE-269 (Improper Privilege Management). In `packages/sso/src/routes/sso.ts`, the `registerSSOProvider` handler fetched the member row to confirm the caller belonged to the org, but the code path stopped there and never called `isOrgAdmin` or any equivalent role check before writing the new provider row to the database.
The patch (PR #9220, commit 86765f1) extracts a `hasOrgAdminRole(member)` helper from `providers.ts` and calls it inside `registerSSOProvider` immediately after the membership lookup. If `member.role` is not `owner` or `admin`, the handler now throws a 403 before persisting anything.
The check is gated on `hasPlugin("organization")` so installations without the org plugin are unaffected.
The fix
Upgrade to `@better-auth/sso@1.6.11` or later. If an immediate upgrade is not possible: set `sso({ providersLimit: 0 })` to block all user-driven registration, or set `organizationProvisioning: { disabled: true }` to stop SSO callbacks from adding org members.
After upgrading, audit existing `ssoProvider` rows where `organizationId IS NOT NULL` and cross-check each creator's `member.role`; remove rows created by non-admins in two steps (provider row first, then org members added by that provider).
Reported by Nadav0077.
Related research
- critical · 9.6CVE-2026-53513CVE-2026-53513: @better-auth/sso SSRF via Unvalidated OIDC Endpoints
- highCVE-2026-53832: openclaw Trusted-Proxy Identity Header Forgery via Same-Host Loopback
- highCVE-2026-53833: OpenClaw QQBot Incorrect Authorization on /bot-streaming Command
- high · 8.8CVE-2026-53821: openclaw Trusted-Proxy WebSocket Scope Elevation