CVE-2026-2912: the next-auth session leak we found in 3 customer codebases
Same misconfiguration, three different shapes. The fix is a one-liner; the detection is not.
In late April, the next-auth maintainers disclosed CVE-2026-2912: a session-fixation primitive triggered by JWT callbacks that mutate the `session` object without re-asserting the `sub` claim. The CVSS is 7.5. The blast radius depends entirely on how you wrote your `session` callback.
the vulnerable shape
lib/auth.ts (vulnerable)export const authOptions: NextAuthOptions = {
callbacks: {
async session({ session, token }) {
// token.userId was set elsewhere in the callback chain, but never
// re-verified against the persisted session record. An attacker who
// can influence the token via a custom OAuth provider redirect can
// pin token.userId to a target user's id.
session.user.id = token.userId as string;
return session;
},
},
};We surfaced this in three customer codebases within the first 48 hours of the CVE landing. None of them had it because they were lazy — all three had `token.userId` set somewhere reasonable in a `jwt` callback. The chain was: a custom OAuth provider returned a profile object the `profile` callback used unguarded, and `token.userId` was propagated forward without re-asserting `token.sub`. Real session fixation, full account takeover on the affected tenant.
why this is hard to detect with code review
The vulnerable line is a single assignment to `session.user.id`. Every next-auth tutorial in the world does this. Reviewers see it daily; their eyes slide off. The danger is not the assignment itself — it is the unverified provenance of `token.userId`.
Static analyzers cannot help either. The data-flow chain crosses an OAuth provider boundary; a typical tool stops at the framework edge. This is exactly the class of bug an LLM agent eats for breakfast: it reads the callback chain backwards, asks "where does this field originally come from", and traces it to the unguarded `profile` callback.
how brink found it
- authn-01 enumerates auth code paths from your OpenAPI / HAR fixture, identifies that next-auth is in use.
- It reads the callback chain, looks for fields on `session.user` derived from `token.*` that are not `token.sub`.
- It hypothesizes a fixation primitive, then validates by attempting to redirect with a forged provider response in a fresh sandbox.
- If the redirect lands and the resulting session has the wrong `user.id`, finding ships with a captured response.
the fix
lib/auth.ts (patched)callbacks: {
async session({ session, token }) {
// Always trust token.sub over any derived field; cross-check against
// the persisted session row so an attacker cannot pin a different id
// by influencing the OAuth profile callback.
const sub = token.sub;
if (!sub) throw new Error('session token missing sub');
session.user.id = sub;
return session;
},
},If you use next-auth, upgrade to 4.24.10 or 5.0.0-beta.21. If you cannot upgrade today, the callback patch above neutralizes the primitive in the common case.