Two CVE identifiers circulated for a single flaw. One of them does not exist.
On September 30, 2026, Better Auth โ a TypeScript authentication framework increasingly embedded across the Node.js and Next.js ecosystem โ shipped a fix for a vulnerability tracked as GHSA-965c-763c-88jm, scored CVSS 9.1. Critical. The flaw permits complete account takeover without ever touching a victim's inbox. Meanwhile, a phantom identifier, CVE-2026-110862, floated through downstream channels carrying an inflated 9.3 score. The official GitHub advisory held firm at 9.1 and the correct GHSA number.
I have spent the better part of a decade reading exploit disclosures the way other people read earnings reports. The number is never the story. The mechanism is. And this mechanism โ OAuth state reuse through a shared verification store โ is one I have watched the crypto world flirt with for years under a different name: cross-chain replay. Same disease. Different immune system.
Better Auth is developer infrastructure. It is not a consumer product with a login page and a mascot. It is a library you install with a single command, wire into a Next.js or Node.js application, and then forget about. That forgetting is the entire value proposition. It handles the unglamorous plumbing of modern authentication: OAuth flows through Google, GitHub, or Apple; Magic Link email sign-in; session management; adapter support across a dozen databases.
The market it plays in is crowded and unforgiving. Auth0 and Clerk sell managed authentication and charge a premium for the privilege of not thinking about it. Supabase Auth bundles identity into a broader backend. NextAuth, now Auth.js, occupies the free tier with a sprawling community. Better Auth's wedge is control: self-hosted, type-safe, open source, no per-month-active-user tax. In a world of tightening data-sovereignty rules, that positioning has structural demand. An identity layer you run yourself is an identity layer no vendor can switch off.
But authentication frameworks carry an uncomfortable property. Their entire reason to exist is a security guarantee. A component library can be buggy and survive. A UI kit can ship a broken dropdown and patch it on Tuesday. An authentication framework cannot. Its only real product is the assurance that the entity on the other end of a session is who it claims to be. When that assurance fractures, the framework does not have a quality problem. It has an existential one.
The adoption curve matters here. A framework nobody uses is a framework whose vulnerabilities hurt nobody. Better Auth is described as increasingly used across the Node.js and Next.js ecosystem, and that growth is exactly what makes this disclosure consequential. Popularity and attack surface move together. The more applications embed a framework, the more incentive security researchers have to pick it apart, and the more catastrophic any single flaw becomes. Two OAuth state vulnerabilities in rapid succession is what that scrutiny looks like from the outside. It is not necessarily a sign of a bad framework. It is a sign of a framework that has crossed the threshold from obscure to worth attacking.
Better Auth has now disclosed two OAuth state-related vulnerabilities in short order: one affecting versions prior to 1.6.2, and this one, addressed in 1.7.7. Two data points on the same axis are not a coincidence. They are a pattern. And patterns are where I stop reading the press release and start reading the code.
OAuth is a delegation protocol. You ask a user to prove identity through a third party, and in exchange you receive a token that says, in effect, 'this person authenticated elsewhere.' The protocol carries a well-known weakness. Without protection, an attacker can initiate a flow, capture the authorization code, and inject it into a victim's session. The industry's answer is the state parameter: a random value your application generates before the redirect and verifies when the flow returns. If the state does not match what you issued, the flow is rejected. It is a nonce with a single job โ to prove that the response belongs to the request you started.
Magic Link authentication solves a different problem. Instead of a password or a third-party provider, the application emails a one-time token. Click it, and you are logged in. That token is a bearer credential: whoever holds it, wins. Structurally, it is also a verification record โ a value generated, stored, and later looked up to prove a claim. It lives in the same conceptual category as the OAuth state, even though the two exist to do completely different things.
Here is where Better Auth's architecture folded. Both the OAuth state value and the Magic Link token were written into the same verification storage. The framework abstracted them into a single lookup table, distinguished โ if at all โ by nothing more than the value itself. No namespace. No purpose binding. No isolation between two fundamentally different security contexts.
The bug was not a missing check. It was a missing boundary.
Walk the attack chain. An attacker initiates an OAuth flow against a target service built on Better Auth. The framework dutifully generates a state value and stores it. The attacker does not complete the flow. They capture the stored value instead. Now they arrange for that same verification store to be queried in the Magic Link context. Because the store does not distinguish purpose, the value minted for one protocol is accepted as a credential for another. The attacker presents it. The session is granted. No inbox access required. No phishing page. No social engineering. The two protocols shared a lock, and the attacker held a key cut for the other door.
This is cross-protocol token confusion, and it is not exotic. It is the authentication equivalent of a replay attack โ the same primitive that has drained bridges and rekt naive smart contracts since the earliest days of on-chain finance. When I reverse-engineered early Uniswap v2 contracts for my thesis, the edge case that mattered most was never an obvious arithmetic error. It was always a context assumption: a value trusted in a place it was never meant to be valid. The mechanism is identical here. A verification record without a context binding is a skeleton key.
The origin of the flaw is worth naming precisely, because it will recur. A shared verification store is a classic over-abstraction. The reasoning is seductive: both OAuth state and Magic Link tokens are 'things we generate, store, and check later.' So a well-intentioned engineer builds one store, one table, one lookup path, and congratulates themselves on DRY code. The abstraction holds until two different security contexts collide in the same namespace. Then the shared store stops being elegant and becomes a confusion primitive. Good abstractions hide complexity. Bad abstractions hide security boundaries. This one hid a boundary.
The advisory also flags a pre-hijacking variant, and this is the more insidious one. Rather than taking over an existing account, the attacker pre-creates a verified account against an email that does not yet have one โ or does not yet exist. When the legitimate owner later registers, they inherit an account the attacker already controls. Microsoft documented this attack family in 2022. It is not merely a bug. It is a race condition against reality: claim the identity before its owner arrives. And because the vulnerable code bypassed protections intended for unverified accounts, the attacker could plant a verified foothold on a mailbox that had never logged in.
Why does this score 9.1? Because the attack chain is low-complexity, requires no privileges, and may require no interaction. The attacker needs exactly one capability: the ability to initiate a flow. That is the same capability every legitimate user already has. When the cost of an attack collapses to the cost of normal use, the CVSS number climbs. The 'no email access required' detail is what separates this from a hundred forgettable token bugs. Account takeover that bypasses the inbox bypasses the last line of defense most users actually possess.
The remediation is, by the standards of security engineering, textbook. Better Auth introduced namespace isolation: verification records are now tagged by purpose, so an OAuth state and a Magic Link token can never be mistaken for one another. More tellingly, the fix introduces per-purpose encryption keys. You do not separate keys unless the original design failed to separate them. The need for distinct cryptographic material per flow is an admission that the old architecture relied on a shared secret, or something uncomfortably close to it. And it raises a question the disclosure does not answer: if isolation was thin here, where else is it thin?
There is a genuine compliment buried in the fix, and it deserves saying because it is rare. The namespace prefixing, the per-purpose keys, and the zero-migration constraint together represent a team that understood the difference between patching a symptom and repairing a model. Many frameworks would have shipped a conditional check โ 'reject state values in the Magic Link path' โ and called it done. That fixes the instance. It does not fix the class. Better Auth chose to fix the class, which is why the next disclosure, if it comes, is less likely to be the same shape. That is the difference between a team that reads its own advisory and a team that writes one.
The team also engineered the fix to require no schema migration and no user migration. That is not a convenience. It is a strategic decision. Authentication layers sit on session tables, user records, and OAuth configurations. Ripping them out is expensive enough that 'the security hole is uncomfortable, but migrating is worse' becomes a genuine organizational posture. By eliminating migration friction, Better Auth removed the excuse. A patch users will actually apply is worth more than an elegant patch they postpone.
The cost lands elsewhere. The fix requires a coordinated switch, which means that during the upgrade window, all historical Magic Link tokens are invalidated and any pending OAuth or SAML logins must be re-initiated. That is the hidden tax of stateful verification records. They are not stateless tokens you can rotate silently. They are rows in a table, and changing the schema of trust means someone, somewhere, gets logged out.
For operators who cannot immediately upgrade, the published mitigations expose the configuration surface underneath: account.storeStateStrategy, storeToken, and verification.storeIdentifier. To degrade safely, an engineer must reason about three separate layers โ state strategy, token storage, and identifier namespacing. A framework that requires its users to understand namespace isolation to stay secure has default settings that are not robust enough. That is a design critique, not a nitpick. The median developer integrating an authentication library will not read the advisory carefully. The defaults are the security model for most of the installed base.
Here is the part that should worry the ecosystem more than any single advisory. Keycloak and BotSharp have exhibited the same class of problem. That is not three coincidences. That is an anti-pattern: the shared verification store, an abstraction that buys developer convenience by collapsing distinct security contexts into one bucket. I have watched the same shape of failure in my own domain. Cross-chain bridges that reuse message formats across chains, that verify a payload without checking which protocol minted it โ that is the crypto version of this exact bug. Follow the gas, not the hype. The hype said interoperability. The gas said replay. The mechanism never changes: trust a value without binding it to the context that created it.
And then there is the disclosure itself. A CVE that does not exist โ CVE-2026-110862 โ circulated with a 9.3 score attached. The official advisory used a different identifier and held at 9.1. Downstream developers who searched the phantom number found nothing and, worse, may have concluded there was nothing to fix. Alpha hides in the margins, and so do the failures. A polluted advisory supply chain delays patching, and delay is exposure. In an era of AI-generated security writeups, the integrity of the identifier layer is now a supply-chain risk in its own right. If you cannot trust the number, you cannot trust the fix.
The downstream consequences deserve their own line. Every application that embedded a vulnerable version inherited a data-protection exposure it did not create. Under regimes like GDPR, an account takeover is a personal-data security incident, and the obligation to assess breach-notification duties falls on the data controller โ the application, not the framework. Better Auth can ship a perfect patch and still leave hundreds of integrators holding a compliance question they never asked for. This is the asymmetry of dependency: the risk flows downstream faster than the fix does.
Let me put probabilities on the board, because binary 'safe or unsafe' framing is how people get hurt. The probability of a further related disclosure in this framework over the next two quarters is not negligible โ I would place it above even odds, given two prior hits on the same axis. The probability that a downstream application running an affected version failed to patch within thirty days is higher still, because the phantom CVE actively confused the search path. The probability that at least one of those unpatchable-in-time deployments suffered a real compromise is unknown and, by nature, underreported. None of these probabilities are zero, and each one compounds the last.
There is a version of this story where the vulnerability is boring. In that version, Better Auth is a niche library, the flaw is caught in a lab, and the patch ships before anyone builds an exploit. That is not this version. This version has a public advisory, a phantom duplicate, and a pre-hijacking variant that targets accounts before they exist. The boring version requires that the ecosystem's information layer works. The version we got suggests it does not, at least not reliably. And that is the finding that outlasts the patch.
Watch where this lands. Authentication frameworks are the quiet dependency under a surprising amount of crypto infrastructure โ exchange dashboards, custodial front-ends, portfolio trackers, the SaaS layer that sits around every protocol. A cross-protocol token confusion flaw does not care whether the session it grants belongs to a fintech app or a DeFi console. It grants the session. For a custodial service, a single confused verification record can mean an account takeover that touches real balances. Data does not care about your narrative. Neither does a session. The framework does not know it is protecting money. It only knows it verified a value.
Correlation is not causation, and the loudest conclusion here is the wrong one. The tempting read is 'Better Auth is insecure.' The data does not support that. The data supports something narrower and more interesting: Better Auth made a design trade that the entire authentication-framework category makes, and it got caught first. Keycloak, a decade-old enterprise standard with far more scrutiny behind it, has the same class of flaw. If maturity were protection, Keycloak would not appear in the same paragraph.
The real contrarian point is that the code is not the story. Code does not lie; people do โ but here the code was honest and the surrounding information layer was not. The fix is clean. The engineering is competent. The disclosure is competent. What failed was the ecosystem around it: a fabricated CVE, an inflated score, a confused downstream that could not find the right patch because it was searching for the wrong number. The vulnerability did less damage to Better Auth's reputation than the polluted advisory chain did to its users' ability to respond.
So the uncomfortable conclusion: the most dangerous actor in this event was not the framework. It was the noise. And Better Auth's security is now a function of how fast that noise clears, not how good the patch is.
The signal to watch is not whether Better Auth patches again โ it will. The signal is whether the next disclosure arrives with one clean, verifiable identifier, or with another phantom number that sends developers down a dead end. Track the advisory, not the announcement. Watch whether the category treats the shared verification store as a one-off mistake or a systemic anti-pattern worth auditing across every framework that has quietly made the same trade. Because in authentication, the mechanism is the message, and the message is the only thing standing between a live session and a skeleton key.