CVE-2026-53513: @better-auth/sso SSRF via Unvalidated OIDC Endpoints
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
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.
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.
Related research
- high · 7.7CVE-2026-53514CVE-2026-53514: better-auth Organization Invitation Takeover via Unverified Email Pre-Registration
- high · 8.3CVE-2026-53516CVE-2026-53516: better-auth OAuth Auto-Link Account Takeover via Unverified Email
- high · 7.4CVE-2026-49857CVE-2026-49857: auth-fetch-mcp SSRF Protection Bypass via IPv4-mapped IPv6 Loopback
- high · 8.8CVE-2026-55698CVE-2026-55698: pnpm Env Lockfile Package-Manager Integrity Bypass