# @better-auth/scim: Account/provider takeover via missing owner binding on non-org SCIM providers

Default @better-auth/scim config leaves non-org SCIM providers ownerless, letting any authenticated user regenerate another user's SCIM bearer token and take over the provider.

- Payout: Research
- Severity: high
- Program: Better Auth
- Found by: Jvr2022
- Disclosed: 2026-07-07
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-j8v8-g9cx-5qf4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-j8v8-g9cx-5qf4
- Tags: area:web domain:api domain:web impact:ato impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:access-control vuln:account-takeover vuln:auth-bypass vuln:idor

**Target:** SCIM plugin management endpoints (/scim/generate-token, /scim/get-provider-connection)
**Root cause:** Non-org SCIM providers are created without a userId because providerOwnership defaults to off; the access check only denies when a stored owner exists and differs, so ownerless rows pass for any caller.
**Steps:**
1. Alice signs in and creates a non-org provider: `POST /scim/generate-token` with `{ "providerId": "corp-idp" }`
2. Bob signs in and reads Alice's provider: `GET /scim/get-provider-connection?providerId=corp-idp` returns `200` with `{ "providerId": "corp-idp" }`
3. Bob regenerates the token: `POST /scim/generate-token` with `{ "providerId": "corp-idp" }` returns `201` and a new `scimToken`
4. Alice's old token is rejected at `GET /scim/v2/Users` (`401`); Bob's regenerated token is accepted (`200`)
**How found:** Code review of the non-organization branch of the access check: the guard `provider.userId && provider.userId !== userId` is false when userId is unset, and the connection list filter `p.userId === userId || !p.userId` explicitly includes ownerless rows.
**Exploitation:** Attacker with any authenticated session enumerates or guesses a providerId, calls the generate-token endpoint; the plugin deletes the ownerless row, creates a fresh one and returns a new SCIM bearer token to the attacker, invalidating the victim's token. Attacker then authenticates to the provider's SCIM API routes and manages provisioned users, or deletes the connection.
**Escalation:** Token regeneration pivots from metadata read to full SCIM API access (user provisioning/management) on the victim's IdP connection.
**Insight:** When an ownership column is optional and the authz check is written as 'deny only if owner set and mismatched', every legacy/default row is a shared-key object; grep for `!row.ownerId` style filters and for authz guards gated on truthiness of the owner field.

## Proof of concept (copied exactly from the original report)

**Bob regenerates Alice's SCIM token**

```
3. Bob regenerates the token: `POST /scim/generate-token` with `{ "providerId": "corp-idp" }` returns `201` and a new `scimToken`.
```

**Bob reads Alice's provider connection**

```
2. Bob signs in and reads Alice's provider: `GET /scim/get-provider-connection?providerId=corp-idp` returns `200` with `{ "providerId": "corp-idp" }`.
```

**Alice creates non-org provider**

```
1. Alice signs in and creates a non-org provider: `POST /scim/generate-token` with `{ "providerId": "corp-idp" }`.
```

