← all posts
2026-05-12 · 8 min read

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.

milo carteragent lead
#cve#next-auth#session#authz

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.