On 11 August 2026 Better Auth published GHSA-8c5h-wx78-2cfg, a CVSS 8.1 advisory against @better-auth/sso. The summary is one sentence: an organization owner could register an SSO provider for a domain they did not own, and users with that email domain got quietly added to their organization.
We pulled both builds off npm and diffed them. The advisory is accurate, and it undersells two things: what triggers the assignment, and how little configuration you needed to be exposed.
TL;DR
@better-auth/sso at 1.6.26 and earlier looked up organization membership by email domain and only required that domain to be verified if you had turned domain verification on. It is off by default. The hook fired on /callback/, Better Auth's social sign-in path, so a plain Google login was enough. Upgrade to 1.6.27, then audit your member table, because the patch does not undo memberships already written.
The one line that mattered
Here is the provider lookup from @better-auth/sso 1.6.26, taken from the published bundle:
const domain = user.email.split("@")[1];
if (!domain) return;
const whereClause = [{ field: "domain", value: domain }];
if (domainVerification?.enabled) whereClause.push({
field: "domainVerified",
value: true
});
let ssoProvider = await ctx.context.adapter.findOne({
model: "ssoProvider",
where: whereClause
});
The domainVerified filter is conditional. It is pushed onto the query only when domainVerification.enabled is true, and Better Auth's SSO documentation says that flag "is disabled by default and must be explicitly enabled."
So on a stock configuration the query reduces to "find me any SSO provider whose domain column equals this user's email domain." Nobody checks who put that row there.
Registering an SSO provider is an authenticated action available to an organization owner. On the vulnerable versions, the domain value on that provider was accepted as a claim, not a proof. Write acme.com into it and every user whose email ends in @acme.com becomes a member of your organization at defaultRole: "member".
It fired on Google sign-in, not just SSO
This is the finding that changed how serious we think this is, and it comes from Better Auth's own code rather than from the advisory.
The function is invoked from an after middleware with this matcher:
after: [{
matcher(context) {
return context.path?.startsWith("/callback/") ?? false;
},
handler: createAuthMiddleware(async (ctx) => {
const newSession = ctx.context.newSession;
if (!newSession?.user) return;
if (!ctx.context.hasPlugin("organization")) return;
await assignOrganizationByDomain(ctx, {
user: newSession.user,
provisioningOptions: options?.organizationProvisioning,
domainVerification: options?.domainVerification
});
})
}]
/callback/ is Better Auth's social provider callback, not /sso/callback/. The library's own docstring on the function says so in plain language:
This enables domain-based org assignment for non-SSO sign-in methods (e.g., Google OAuth with @acme.com email gets added to Acme's org).
That is a documented feature, not an accident. The bug is that the feature trusted an unverified claim. So the population at risk was never "our SSO users." It was every user who signed in with Google or GitHub while the SSO plugin and the organization plugin were both installed.
You did not have to configure anything
Look at what the handler passes down: provisioningOptions: options?.organizationProvisioning. If you never configured organizationProvisioning, that value is undefined.
Now look at the first guard inside the vulnerable function:
if (provisioningOptions?.disabled) return;
if (!ctx.context.hasPlugin("organization")) return;
undefined?.disabled is undefined, which is falsy, so the function does not return. Org provisioning was on unless you turned it off. Installing two plugins was the entire setup.
The three conditions for exposure were: @better-auth/sso at 1.6.26 or below, the organization plugin present, and a user signing in through a social provider. No SSO configuration, no organizationProvisioning block, and no domain verification setting were required.
One provider row could claim several companies
A detail worth knowing if you are auditing rather than just upgrading. The domain field is parsed as a comma-separated list:
const entries = domain.split(",").map((entry) => entry.trim()).filter(Boolean);
A single provider registration carrying acme.com,globex.com,initech.com claims all three at once. When you audit your ssoProvider table, read the whole domain string rather than assuming one row means one domain.
The 1.6.26 domain extraction is also worth a glance. user.email.split("@")[1] takes the second element of the split, so it is only correct for addresses with exactly one @. The fixed version replaces it with a getEmailDomain helper that rejects anything that does not split into exactly two non-empty parts, and also rejects domains containing /, \ or :. We did not find a way to get a multi-@ address past Better Auth's own validation, so treat that one as hardening rather than a second exploitable path.
What 1.6.27 actually changed
The patch replaces assignOrganizationByDomain with a rewritten assignOrganization that returns a named outcome instead of falling through silently. For the domain-match path it added five checks that were not there before:
Guards added in 1.6.27
The re-read of the canonical user matters more than it looks. The old code trusted the user object handed to it by the session; the new code fetches the row by id before making any decision.
Check yours
Get the resolved version, not the range
npm ls @better-auth/sso
Read the version npm prints rather than the caret range in package.json. A stale lockfile is the usual reason a project on ^1.6.0 is still resolving something from July.
Upgrade
npm install @better-auth/sso@^1.6.27
On the 1.4 line the fixed release is 1.4.8. On the 1.7 prereleases it is 1.7.0-rc.5.
Audit the provider table
Look at every row in ssoProvider and confirm you recognise the domain value, remembering it may hold a comma-separated list. Any domain your team did not register is the thing this advisory is about.
Audit memberships
Upgrading changes nothing about rows already written. Cross-check member against your invitation records and look for users who hold membership without ever having accepted one.
Sources
| Claim | Source | Date |
|---|---|---|
| Advisory, CVSS 8.1, affected and patched ranges | GHSA-8c5h-wx78-2cfg | 2026-08-11 |
| CVE id and CVSS 4.0 score of 8.6 | GHSA-xfj7-7fp8-rhvp / CVE-2026-80192 | NVD 2026-08-26, GitHub DB 2026-08-29 |
| Domain verification is disabled by default | Better Auth SSO plugin docs | Retrieved 2026-09-10 |
Vulnerable lookup, hook matcher, docstring, split("@")[1] | @better-auth/sso 1.6.26, dist/index.mjs | npm published 2026-08-04 |
Rewritten assignOrganization and its five guards | @better-auth/sso 1.6.27, dist/index.mjs | npm published 2026-08-11 |
Both package builds were downloaded from the npm registry and diffed on 2026-09-10. Every code excerpt above is quoted from the published bundle rather than from the repository, so it is the code that actually shipped.
Which versions of Better Auth SSO are affected?
The @better-auth/sso package is affected from 1.4.8-beta.1 up to and including 1.6.26, and in the 1.7 prerelease line from 1.7.0-beta.0 through 1.7.0-rc.4. Fixed releases are 1.4.8, 1.6.27 and 1.7.0-rc.5. Core better-auth is a separate package and is not what this advisory covers, so check the SSO plugin's own version rather than your main auth dependency.
Am I affected if my users do not use SSO?
Possibly yes, and this is the part the advisory text does not spell out. In 1.6.26 the org-assignment hook ran on paths starting with /callback/, which is Better Auth's social sign-in callback rather than the SSO callback. Installing the SSO plugin alongside the organization plugin was enough for an ordinary Google or GitHub sign-in to trigger domain-based org assignment.
Is domain verification on by default in Better Auth?
No. Better Auth's SSO documentation states that domain verification is disabled by default and must be explicitly enabled with domainVerification.enabled set to true. In the vulnerable versions that default was what removed the domainVerified filter from the provider lookup, so the shipped default was the unsafe path.
How do I check which version I have?
Run npm ls @better-auth/sso to see the resolved version rather than the range in your package.json, because a caret range can resolve to an old version from a stale lockfile. If it prints anything at or below 1.6.26, upgrade to 1.6.27 and then audit existing organization memberships, since upgrading does not remove memberships that were already created.
Does upgrading undo memberships that were already created?
No. The patch changes how future assignments are made and does nothing to rows already written to your member table. If you ran an affected version with the organization plugin installed, review existing memberships for users who never accepted an invitation, and check your ssoProvider table for domain values nobody on your team registered.
Check What Your Auth Actually Allows
A free CheckYourVibe scan looks for the exposure patterns AI-generated auth code leaves behind.