Where
-Infinity
0
Severity
9.6
Input Validation, SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Am I affected?

Users are affected if all of the following are true:

- Their application uses @better-auth/sso at a version >= 0.1.0, < 1.6.11 on the stable line, or any 1.7.0-beta.x on the pre-release line. - The sso() plugin is added to their application's betterAuth({ plugins: [...] }) array. - Any user with a valid Better Auth session can reach POST /sso/register (the plugin's default gate accepts any session).

For the non-blind SSRF impact (full IAM credential or internal HTTP body exfiltration), no further configuration is required.

For the account takeover escalation, additionally:

- Developers set sso({ trustEmailVerified: true, ... }). - The developer's application deployment has accounts whose email overlaps with attacker-chosen domains.

If developers do not enable the SSO plugin, their application is not affected.

Fix:

1. Upgrade to @better-auth/sso@1.6.11 or later. 2. If developers cannot upgrade, see workarounds below.

Summary

The @better-auth/sso plugin's POST /sso/register endpoint accepts attacker-controlled oidcConfig.userInfoEndpoint, tokenEndpoint, and jwksEndpoint URLs when skipDiscovery: true is set, persists them on the ssoProvider row without origin validation, then issues server-side fetches to those URLs during the OIDC callback. The fetched response body is reflected through the user profile, producing a non-blind SSRF reachable by any authenticated session. The same primitive exists on POST /sso/update-provider.

Details

The schema field types accept bare strings: no .url() validator, no origin gate. The discovery branch (skipDiscovery: false) routes URLs through validateDiscoveryUrl; the skip-discovery branch persists them as-is. At callback time three fetch sites read the stored URLs: validateAuthorizationCode for the token endpoint, betterFetch for the userInfo endpoint, and validateToken for the JWKS endpoint.

When trustEmailVerified: true is configured, the attacker can escalate to account linking. A malicious userInfo response with emailVerified: true and a chosen email triggers OAuth auto-link against any pre-existing user row with that email, compounding the SSRF into account takeover.

Patches

Fixed in @better-auth/sso@1.6.11. Provider registration (POST /sso/register with skipDiscovery: true) and every POST /sso/update-provider request now validate each supplied OIDC endpoint URL (authorizationEndpoint, tokenEndpoint, userInfoEndpoint, jwksEndpoint, discoveryEndpoint) at registration time. A URL is rejected unless it satisfies one of two conditions:

1. Its host is publicly routable on the internet, evaluated through the @better-auth/core/utils/host.isPublicRoutableHost gate. RFC 1918 private ranges, RFC 4193 unique-local addresses, link-local addresses (including the cloud-metadata IP 169.254.169.254), loopback, multicast, broadcast, and reserved ranges are rejected, along with cloud-metadata FQDNs. 2. Its origin is already listed in the application's trustedOrigins configuration. This preserves the documented escape hatch for customers running internal IdPs intentionally on private networks.

The schema also tightens from z.string() to z.url() on those fields, so malformed URLs fail at parse time rather than at fetch time. Deployments running internal IdPs that previously worked must add the IdP's origin to trustedOrigins to keep working after upgrade.

Workarounds

If developers cannot upgrade immediately:

- Disable provider self-registration: set sso({ providersLimit: 0 }). The limit is enforced before the schema branch, blocking every /sso/register regardless of skipDiscovery. - Reverse-proxy gate: block POST /sso/register and POST /sso/update-provider at the edge, or restrict to a denylist of source IPs and a small admin user list. - Network-level egress controls: block egress from the auth server to RFC 1918, RFC 4193, link-local ranges (169.254.0.0/16, fe80::/10), and the cloud-metadata FQDN list at the firewall or VPC level. AWS users should additionally enforce IMDSv2 (HttpTokens: required). - Set trustEmailVerified: false until upgrade. This caps the impact at non-blind SSRF and removes the account-takeover escalation, but does not stop the SSRF.

Impact

- Server-Side Request Forgery (non-blind): the attacker reads response bodies from any HTTP endpoint reachable from the auth server, including cloud metadata services (AWS IMDS, GCP metadata FQDN), internal-only APIs, and infrastructure services such as Redis or admin panels bound to localhost. - Account takeover (when trustEmailVerified: true): the attacker mints a malicious userInfo response asserting emailVerified: true for an arbitrary email, triggering OAuth auto-link against pre-existing user rows.

Credit

Reported by Vaadata.

Resources

- CWE-918: Server-Side Request Forgery (SSRF) - CWE-20: Improper Input Validation - CWE-441: Unintended Proxy or Intermediary - CWE-345: Insufficient Verification of Data Authenticity

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N

Am I affected?

You are affected if all of the following are true:

- You depend on @better-auth/sso at any version in >= 1.2.10, < 1.6.11, or any current next pre-release. - You enable both sso() and organization() plugins. - providersLimit is at its default (10) or any non-zero value, so SSO provider registration is enabled for authenticated users. - An organization has non-admin members, or your application can add users to organizations as regular members.

You are at the highest risk if any of these also hold:

- Organization membership can be obtained without a direct admin decision, such as through open invitations, self-serve onboarding, public team joins, or SCIM bulk imports. - organizationProvisioning.defaultRole or organizationProvisioning.getRole returns admin or higher for SSO-provisioned users. The bug then becomes unauthorized admin creation in the org. - domainVerification.enabled is false (the default). The malicious provider is immediately usable.

Fix:

1. Upgrade to @better-auth/sso@1.6.11 or later. 2. If you cannot upgrade, see workarounds below.

Summary

The SSO plugin's POST /sso/register endpoint lets any member of an organization attach a new SSO provider to that organization. It checks that the caller has a membership row, but it does not check whether the caller has an administrative role for the organization.

This creates an authorization mismatch for the same resource. Other org-linked SSO provider management endpoints treat those providers as admin-managed: list, get, update, and delete require the caller to be an organization owner or admin. The create path is less restrictive, so a regular member can attach an attacker-controlled OIDC or SAML identity provider to an organization they do not administer. After registration, downstream organization provisioning can add IdP-asserted users from /sso/callback/{providerId} into the target organization, defaulting to role member.

Details

This issue does not rely on a separate documentation statement that only admins may create SSO connections. The issue is that Better Auth already enforces an admin boundary for org-linked SSO provider management, but registerSSOProvider does not enforce the same boundary when the provider is first created.

The list, get, update, and delete endpoints in providers.ts gate org-linked SSO providers via isOrgAdmin, which accepts owner or admin. The create path in sso.ts performs only a membership lookup and never inspects member.role. As a result, the endpoint allows a low-privilege organization member to create a provider record that they would not be allowed to view, update, or delete through the companion provider-management endpoints.

The fix introduces a shared hasOrgAdminRole(member) helper (refactored out of isOrgAdmin) and adds the admin check to the registration handler so that registration matches the protections on the read and mutation paths.

Patches

Fixed in @better-auth/sso@1.6.11. When organizationId is supplied and the organization plugin is enabled, the registerSSOProvider handler now requires the caller to hold the owner or admin role on the target organization. This makes provider creation match the existing protection on the get, update, and delete endpoints.

Workarounds

If you cannot upgrade immediately:

- Disable user-driven SSO registration entirely: set sso({ providersLimit: 0 }). Registration throws FORBIDDEN before the membership gate. Trade-off: admins also lose self-serve provider creation; provisioning has to go through server-side auth.api.registerSSOProvider({ headers: serverAdminHeaders, body }) calls. - Disable SSO-driven org provisioning: set sso({ organizationProvisioning: { disabled: true } }). The malicious provider can still be registered, but the SSO callback no longer adds users to the org automatically. Trade-off: legitimate SSO-driven onboarding stops. - Custom before hook on /sso/register that asserts the caller's role on body.organizationId is owner or admin. This duplicates the patch shape in user code. - Audit existing rows: list ssoProvider rows where organizationId IS NOT NULL, cross-reference each userId with member.role for that organization, and remove provider rows whose creator is not currently owner or admin. Removing the provider does not auto-remove members it created, so cleanup is two-step.

Impact

- Unauthorized provider configuration within an organization tenant: a regular member writes an SSO provider record on an organization they do not administer. - Unauthorized organization membership creation: subsequent SSO callbacks can add IdP-asserted users to the target organization at the configured default role. - Admin creation when configured: if organizationProvisioning.defaultRole is admin, or organizationProvisioning.getRole returns admin, the issue can create admin-grade users in the target organization without owner consent.

Credit

Reported by @Nadav0077. The same finding was previously raised in public issue #9133 (2026-04-12).

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203