On 2026-07-23 an Auth.js advisory landed with an unusual property: the same identifier is scored 9.1 Critical by GitHub's Advisory Database and Low by the Auth.js maintainers themselves.
Both are defensible, and the gap between them is the useful part. This is not a hole that anyone on the internet can walk through. It's a bug that converts a broken deploy into an open app, silently, in exactly the situation where you'd expect the opposite.
TL;DR
In next-auth v5 beta up to 5.0.0-beta.31, a server-side configuration error populates the auth object with an error object instead of leaving it null. Error objects are truthy. So if (req.auth) returns true for every request, including unauthenticated ones. Three conditions must all hold: an affected version, a bare existence check, and a genuinely broken config. Fixed in 5.0.0-beta.32. The one-line mitigation is !!req.auth?.user instead of !!req.auth.
The three conditions
The advisory states plainly that there is no impact while the configuration is valid. That single sentence is why the severity is argued about, and it's the first thing to check.
| Condition | What it means for you |
|---|---|
| Affected version | next-auth >= 5.0.0-beta.0 and <= 5.0.0-beta.31 |
| Bare existence check | You gate on if (req.auth) or !!auth, not on a property |
| A real config error | Most commonly AUTH_SECRET unset in that environment |
All three, or you're not exposed. Miss any one and this advisory is not your problem.
That third row is what makes it worth writing about. A missing environment variable is not an exotic failure. It's the single most common thing that goes wrong the first time a founder deploys to Vercel, because the app ran fine locally where .env.local existed.
The failure mode is inverted from what anyone expects. Your auth breaks, and instead of everyone being locked out, everyone is let in. The app looks like it's working.
What actually happens
When Auth.js hits a server-side error, @auth/core returns an HTTP 500 whose body is a JSON error description. The literal payload is:
{
"message": "There was a problem with the server configuration. Check the server logs for more information."
}
The next-auth wrapper read that response body without first checking the status code. So instead of auth being null, it became that object. And in JavaScript, an object is truthy no matter what's inside it.
// middleware.ts
export default auth((req) => {
if (req.auth) return // an error object is truthy, so this passes
return NextResponse.redirect(new URL('/login', req.url))
})
The advisory's own wording is that such a check "evaluates to true for every request, including unauthenticated ones."
The two triggers it names are a Keycloak provider missing both its issuer and its authorization endpoint config, and an unset AUTH_SECRET. The release notes for the fix describe the root cause more narrowly than "any error": a non-OK session response now yields no session rather than an error object. The wrapper was parsing a failure as if it were a success.
Classified as CWE-285 (Improper Authorization) and CWE-636 (Not Failing Securely), which is the more precise name for what went wrong.
About that severity
Worth being straight about, because you'll see all three numbers cited:
| Source | Rating |
|---|---|
| GitHub Advisory Database | 9.1 Critical (CVSS 4.0) |
| Auth.js maintainers' own advisory page | Low, no CVSS given |
| A third-party aggregator | 7.4 |
No CVE was assigned, which is part of why coverage has been thin.
The CVSS 4.0 vector carries AT:P, meaning attack requirements are present. That's the formal admission that the bug needs a pre-existing misconfiguration before it does anything. A 9.1 reflects what happens when it fires: full authentication bypass. A Low reflects how often the preconditions hold. Neither is dishonest.
For a founder, the practical reading is: don't panic, do check, and treat it as one more reason that if (auth) was never a good enough guard.
Check it in five minutes
Find your version.
npm ls next-auth
Anything from 5.0.0-beta.0 to 5.0.0-beta.31 is in range. 4.x is not affected by this particular advisory.
Grep for the pattern. This is the part people skip, and it's the one that decides whether the version even matters.
grep -rn "if (req.auth)\|if (auth)\|!!req\.auth\b\|!!auth\b\|if (session)" \
middleware.ts src/middleware.ts app/ src/ 2>/dev/null
Every hit is a place where an error object would have been accepted as a logged-in user.
Confirm your deployed config is actually sound. Local is not the environment that matters. Check that AUTH_SECRET is set in the deployment you care about, and look for [auth][error] lines in your production logs. If they're there, your config is broken right now.
Test the guard from outside. No session, no cookies, straight at a route that's supposed to be protected.
curl -s -o /dev/null -w "%{http_code}\n" https://your-app.com/dashboard
A 200 where you expected a redirect to /login is the answer. This is the check that doesn't care whether you understood the advisory correctly.
The fix
Upgrade, then change the pattern. Both, not either.
npm install next-auth@5.0.0-beta.32
// before: an error object satisfies this
const isLoggedIn = !!req.auth
// after: an error object has no .user
const isLoggedIn = !!req.auth?.user
The advisory also recommends treating [auth][error] log lines as a failed health check, so a misconfigured build cannot silently reach production. That's the durable fix. The version bump closes this instance; failing the deploy on a broken auth config closes the whole class.
The advisory shipped with an unfilled template. Its prose literally reads "This is released in next-auth@<!-- TODO: set patched version on publish -->". The version number 5.0.0-beta.32 comes from the structured metadata and the npm release, not from the write-up. If you went looking for the patched version in the advisory text and found nothing, that's why.
Three more in the same batch
Four Auth.js advisories published together. They have different affected ranges, and mixing them up is the easy mistake here.
- GHSA-7rqj-j65f-68wh (rated Critical in GitHub's DB, High by the maintainers). A Unicode homoglyph email address passes validation, then normalizes into something with multiple
@symbols, so a magic sign-in link routes to the attacker. Zero victim interaction. This one does hit v4, and@auth/coreas well. Fixed in@auth/core0.41.3,next-auth4.24.15, and5.0.0-beta.32. - GHSA-xmf8-cvqr-rfgj (7.5 High, CVSS 3.1).
getToken()URL-decodes the bearer value before validating it, so malformed percent-encoding throws an uncaught exception. Availability only. The advisory is explicit that it does not bypass authentication. - GHSA-x445-f3h2-j279 (Moderate). OAuth state, nonce and PKCE cookies aren't bound to a provider.
All four are fixed by the same upgrade, which is the practical takeaway. One caution if you go read them: two of these carry version ranges in their prose that contradict their own machine-readable ranges. Trust the structured data, not the sentence.
As of 2026-08-03, 5.0.0-beta.32 is still the newest beta on npm and none of these advisories has been withdrawn or rescored. If you are reading this much later, check the current version before acting on the numbers above.
Why this shape keeps recurring
This is the third authorization bypass in three months where the bug wasn't in the auth logic but in the thing that decides whether the auth logic runs. A Next.js middleware bypass. A Nuxt route rule that silently never matched. Now an auth object that's truthy when it should be absent.
The pattern underneath all three: a check that asks did something come back rather than did the right thing come back. Truthiness is not authentication. An error is not a session.
Which is why the durable version of this fix isn't a version number. Put the real authorization decision next to the data it protects, so that when the outer guard misbehaves, something else is still asking who you are.
How do I know if my app is affected?
Three things all have to be true. You are on next-auth 5.0.0-beta.0 through 5.0.0-beta.31. You guard something with a bare existence check like if (req.auth) or !!auth rather than checking a property on it. And your deployed config is actually broken, which most often means AUTH_SECRET is not set in that environment. The advisory is explicit that there is no impact while the configuration is valid. If any one of the three is false, this does not apply to you.
Is this really a 9.1 Critical?
It depends who you ask, and that is worth knowing. GitHub's Advisory Database scores it 9.1 Critical. The Auth.js maintainers' own advisory page for the same GHSA rates it Low with no CVSS number at all. One third-party aggregator publishes 7.4. The CVSS 4.0 vector itself carries AT:P, meaning attack requirements are present, which is the formal way of saying it needs a pre-existing misconfiguration to fire. Treat it as a configuration-dependent bypass rather than a remote hole in every install.
Which version fixes it?
next-auth 5.0.0-beta.32, published to npm on 2026-07-20. The advisory's own prose never names the version, it shipped with an unfilled TODO placeholder where the number should be, so the version comes from the structured metadata and the release notes rather than the write-up. The release notes describe the fix as making a non-OK session response yield no session instead of an error object.
Is next-auth v4 affected by this one?
No. This advisory covers a single range, 5.0.0-beta.0 through 5.0.0-beta.31. Be careful here, because two other Auth.js advisories published in the same batch do carry v4 ranges, and conflating them is easy. If you are on v4 you should still read about those, but the fail-open issue is not one of them.
Does this affect SvelteKit Auth or Express Auth?
The advisory names only the next-auth package. It does not list @auth/core, @auth/sveltekit, @auth/express, or @auth/qwik. That is not the same as a clean bill of health: the fix shipped inside @auth/core 0.41.3, and the root cause is a wrapper not checking the HTTP status of a session response, which is a shape other wrappers could share. They simply were not assessed. Upgrade anyway and use a property check.
Does your protected route answer a stranger?
A scan requests your app's routes with no session at all and reports the ones that return a page anyway. That check does not care which advisory caused it.