VYPR
Moderate severityNVD Advisory· Published Nov 20, 2023· Updated Aug 29, 2024

next-auth vulnerable to possible user mocking that bypasses basic authentication

CVE-2023-48309

Description

NextAuth.js provides authentication for Next.js. next-auth applications prior to version 4.24.5 that rely on the default Middleware authorization are affected by a vulnerability. A bad actor could create an empty/mock user, by getting hold of a NextAuth.js-issued JWT from an interrupted OAuth sign-in flow (state, PKCE or nonce). Manually overriding the next-auth.session-token cookie value with this non-related JWT would let the user simulate a logged in user, albeit having no user information associated with it. (The only property on this user is an opaque randomly generated string). This vulnerability does not give access to other users' data, neither to resources that require proper authorization via scopes or other means. The created mock user has no information associated with it (ie. no name, email, access_token, etc.) This vulnerability can be exploited by bad actors to peek at logged in user states (e.g. dashboard layout). next-auth v4.24.5 contains a patch for the vulnerability. As a workaround, using a custom authorization callback for Middleware, developers can manually do a basic authentication.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
next-authnpm
< 4.24.54.24.5

Affected products

1

Patches

1
d237059b6d0c

fix: differentiate between issued JWTs

https://github.com/nextauthjs/next-authBalázs OrbánNov 10, 2023via ghsa
3 files changed · +52 17
  • packages/next-auth/src/core/lib/oauth/checks.ts+25 7 modified
    @@ -21,11 +21,17 @@ export async function signCookie(
     
       logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
     
    +  const { name } = cookies[type]
       const expires = new Date()
       expires.setTime(expires.getTime() + maxAge * 1000)
       return {
    -    name: cookies[type].name,
    -    value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
    +    name,
    +    value: await jwt.encode({
    +      ...options.jwt,
    +      maxAge,
    +      token: { value },
    +      salt: name,
    +    }),
         options: { ...cookies[type].options, expires },
       }
     }
    @@ -71,16 +77,18 @@ export const pkce = {
         if (!codeVerifier)
           throw new TypeError("PKCE code_verifier cookie was missing.")
     
    +    const { name } = options.cookies.pkceCodeVerifier
         const value = (await jwt.decode({
           ...options.jwt,
           token: codeVerifier,
    +      salt: name,
         })) as any
     
         if (!value?.value)
           throw new TypeError("PKCE code_verifier value could not be parsed.")
     
         resCookies.push({
    -      name: options.cookies.pkceCodeVerifier.name,
    +      name,
           value: "",
           options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
         })
    @@ -121,12 +129,17 @@ export const state = {
     
         if (!state) throw new TypeError("State cookie was missing.")
     
    -    const value = (await jwt.decode({ ...options.jwt, token: state })) as any
    +    const { name } = options.cookies.state
    +    const value = (await jwt.decode({
    +      ...options.jwt,
    +      token: state,
    +      salt: name,
    +    })) as any
     
         if (!value?.value) throw new TypeError("State value could not be parsed.")
     
         resCookies.push({
    -      name: options.cookies.state.name,
    +      name,
           value: "",
           options: { ...options.cookies.state.options, maxAge: 0 },
         })
    @@ -166,12 +179,17 @@ export const nonce = {
         const nonce = cookies?.[options.cookies.nonce.name]
         if (!nonce) throw new TypeError("Nonce cookie was missing.")
     
    -    const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
    +    const { name } = options.cookies.nonce
    +    const value = (await jwt.decode({
    +      ...options.jwt,
    +      token: nonce,
    +      salt: name,
    +    })) as any
     
         if (!value?.value) throw new TypeError("Nonce value could not be parsed.")
     
         resCookies.push({
    -      name: options.cookies.nonce.name,
    +      name,
           value: "",
           options: { ...options.cookies.nonce.options, maxAge: 0 },
         })
    
  • packages/next-auth/src/jwt/index.ts+13 8 modified
    @@ -15,8 +15,9 @@ const now = () => (Date.now() / 1000) | 0
     
     /** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
     export async function encode(params: JWTEncodeParams) {
    -  const { token = {}, secret, maxAge = DEFAULT_MAX_AGE } = params
    -  const encryptionSecret = await getDerivedEncryptionKey(secret)
    +  /** @note empty `salt` means a session token. See {@link JWTEncodeParams.salt}. */
    +  const { token = {}, secret, maxAge = DEFAULT_MAX_AGE, salt = "" } = params
    +  const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
       return await new EncryptJWT(token)
         .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
         .setIssuedAt()
    @@ -27,9 +28,10 @@ export async function encode(params: JWTEncodeParams) {
     
     /** Decodes a NextAuth.js issued JWT. */
     export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
    -  const { token, secret } = params
    +  /** @note empty `salt` means a session token. See {@link JWTDecodeParams.salt}. */
    +  const { token, secret, salt = "" } = params
       if (!token) return null
    -  const encryptionSecret = await getDerivedEncryptionKey(secret)
    +  const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
       const { payload } = await jwtDecrypt(token, encryptionSecret, {
         clockTolerance: 15,
       })
    @@ -116,12 +118,15 @@ export async function getToken<R extends boolean = false>(
       }
     }
     
    -async function getDerivedEncryptionKey(secret: string | Buffer) {
    +async function getDerivedEncryptionKey(
    +  keyMaterial: string | Buffer,
    +  salt: string
    +) {
       return await hkdf(
         "sha256",
    -    secret,
    -    "",
    -    "NextAuth.js Generated Encryption Key",
    +    keyMaterial,
    +    salt,
    +    `NextAuth.js Generated Encryption Key${salt ? ` (${salt})` : ""}`,
         32
       )
     }
    
  • packages/next-auth/src/jwt/types.ts+14 2 modified
    @@ -17,7 +17,13 @@ export interface JWT extends Record<string, unknown>, DefaultJWT {}
     export interface JWTEncodeParams {
       /** The JWT payload. */
       token?: JWT
    -  /** The secret used to encode the NextAuth.js issued JWT. */
    +  /**
    +   * Used in combination with `secret` when deriving the encryption secret for the various NextAuth.js-issued JWTs.
    +   * @note When no `salt` is passed, we assume this is a session token.
    +   * This is for backwards-compatibility with currently active sessions, so they won't be invalidated when upgrading the package.
    +   */
    +  salt?: string
    +  /** The key material used to encode the NextAuth.js issued JWTs. Defaults to `NEXTAUTH_SECRET`. */
       secret: string | Buffer
       /**
        * The maximum age of the NextAuth.js issued JWT in seconds.
    @@ -29,7 +35,13 @@ export interface JWTEncodeParams {
     export interface JWTDecodeParams {
       /** The NextAuth.js issued JWT to be decoded */
       token?: string
    -  /** The secret used to decode the NextAuth.js issued JWT. */
    +  /**
    +   * Used in combination with `secret` when deriving the encryption secret for the various NextAuth.js-issued JWTs.
    +   * @note When no `salt` is passed, we assume this is a session token.
    +   * This is for backwards-compatibility with currently active sessions, so they won't be invalidated when upgrading the package.
    +   */
    +  salt?: string
    +  /** The key material used to decode the NextAuth.js issued JWTs. Defaults to `NEXTAUTH_SECRET`. */
       secret: string | Buffer
     }
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.