CVE-2022-31093 (published 2022-06-27T22:15Z)

NextAuth.js is a complete open source authentication solution for Next.js applications. In affected versions an attacker can send a request to an app using NextAuth.js with an invalid `callbackUrl` query parameter, which internally is converted to a `URL` object. The URL instantiation would fail due to a malformed URL being passed into the constructor, causing it to throw an unhandled error which led to the **API route handler timing out and logging in to fail**. This has been remedied in versions 3.29.5 and 4.5.0. If for some reason you cannot upgrade, the workaround requires you to rely on Advanced Initialization. Please see the documentation for more.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-754
cwe_name: Improper Check for Unusual or Exceptional Conditions
description: The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product.
extended_description: 
            The programmer may assume that certain events or conditions will never occur or do not need to be worried about, such as low memory conditions, lack of access to resources due to restrictive permissions, or misbehaving clients or components. However, attackers may intentionally trigger these unusual conditions, thus violating the programmer's assumptions, possibly introducing instability, incorrect behavior, or a vulnerability.
            Note that this entry is not exclusively about the use of exceptions and exception handling, which are mechanisms for both checking and handling unusual or unexpected conditions.
         
         

Fix information:
repo_url: https://github.com/nextauthjs/next-auth
commit_details: repo_url: https://github.com/nextauthjs/next-auth
author: Balázs Orbán
author_date: 2022-06-10 13:52:00+02:00
msg: fix: handle invalid `callbackUrl`
repository: repo_url: https://github.com/nextauthjs/next-auth
repo_name: nextauthjs/next-auth
description: Authentication for the Web.
repo_language: TypeScript
owner: nextauthjs
file_changes: filename: errors.md
change_type: ModificationType.MODIFY
diff: @@ -99,7 +99,7 @@ This is required to store the verification token. Please see the [email provider
 
 The Credentials Provider can only be used if JSON Web Tokens are used for sessions.
 
-JSON Web Tokens are used for Sessions by default if you have not specified a database. However, if you are using a database, then Database Sessions are enabled by default and you need to [explicitly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
+JSON Web Tokens are used for Sessions by default if you have not specified a database. However, if you are using a database, then Database Sessions are enabled by default and you need to [explicitly enable JWT Sessions](/configuration/options#session) to use the Credentials Provider.
 
 If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and managed outside of NextAuth.js.
 
@@ -119,13 +119,17 @@ The default `code_challenge_method` is `"S256"`. This is currently not configura
 > If the client is capable of using "S256", it MUST use "S256", as
   S256" is Mandatory To Implement (MTI) on the server.
 
+#### INVALID_CALLBACK_URL_ERROR
+
+The `callbackUrl` provided was either invalid or not defined. See [specifying a `callbackUrl`](/getting-started/client#specifying-a-callbackurl) for more information.
+
 ---
 
 ### Session Handling
 
 #### JWT_SESSION_ERROR
 
-https://next-auth.js.org/errors#jwt_session_error JWKKeySupport: the key does not support HS512 verify algorithm
+JWKKeySupport: the key does not support HS512 verify algorithm
 
 The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using
 
@@ -161,7 +165,7 @@ Make sure the file is there and the filename is written correctly.
 
 #### NO_SECRET
 
-In production, we expect you to define a `secret` property in your configuration. In development, this is shown as a warning for convenience. [Read more](https://next-auth.js.org/configuration/options#secret)
+In production, we expect you to define a `secret` property in your configuration. In development, this is shown as a warning for convenience. [Read more](/configuration/options#secret)
 
 #### oauth_callback_error expected 200 OK with body but no body was returned
 

programming_language: Markdown
filename: errors.ts
change_type: ModificationType.MODIFY
diff: @@ -63,6 +63,11 @@ export class UnsupportedStrategy extends UnknownError {
   code = "CALLBACK_CREDENTIALS_JWT_ERROR"
 }
 
+export class InvalidCallbackUrl extends UnknownError {
+  name = "InvalidCallbackUrl"
+  code = "INVALID_CALLBACK_URL_ERROR"
+}
+
 type Method = (...args: any[]) => Promise<any>
 
 export function upperSnake(s: string) {

programming_language: TypeScript
filename: index.ts
change_type: ModificationType.MODIFY
diff: @@ -108,7 +108,8 @@ export async function NextAuthHandler<
           let signinUrl = `${pages.signIn}${
             pages.signIn.includes("?") ? "&" : "?"
           }callbackUrl=${encodeURIComponent(options.callbackUrl)}`
-          if (error) signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
+          if (error)
+            signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
           return { redirect: signinUrl, cookies }
         }
 

programming_language: TypeScript
filename: assert.ts
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,10 @@ import {
   MissingAuthorize,
   MissingSecret,
   UnsupportedStrategy,
+  InvalidCallbackUrl,
 } from "../errors"
+import parseUrl from "../../lib/parse-url"
+import { defaultCookies } from "./cookie"
 
 import type { NextAuthHandlerParams } from ".."
 import type { WarningCode } from "../../lib/logger"
@@ -18,6 +21,14 @@ type ConfigError =
 
 let twitterWarned = false
 
+function isValidHttpUrl(url: string) {
+  try {
+    return /^https?:/.test(new URL(url).protocol)
+  } catch {
+    return false
+  }
+}
+
 /**
  * Verify that the user configured `next-auth` correctly.
  * Good place to mention deprecations as well.
@@ -44,8 +55,30 @@ export function assertConfig(
     }
   }
 
+  const callbackUrlParam = req.query?.callbackUrl as string | undefined
+
+  if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam)) {
+    return new InvalidCallbackUrl(
+      `Invalid callback URL. Received: ${callbackUrlParam}`
+    )
+  }
+
   if (!req.host) return "NEXTAUTH_URL"
 
+  const url = parseUrl(req.host)
+
+  const { callbackUrl: defaultCallbackUrl } = defaultCookies(
+    options.useSecureCookies ?? url.base.startsWith("https://")
+  )
+  const callbackUrlCookie =
+    req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
+
+  if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie)) {
+    return new InvalidCallbackUrl(
+      `Invalid callback URL. Received: ${callbackUrlCookie}`
+    )
+  }
+
   let hasCredentials, hasEmail
   let hasTwitterOAuth2
 

programming_language: Scala
filename: cookie.ts
change_type: ModificationType.MODIFY
diff: @@ -68,6 +68,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
     callbackUrl: {
       name: `${cookiePrefix}next-auth.callback-url`,
       options: {
+        httpOnly: true,
         sameSite: "lax",
         path: "/",
         secure: useSecureCookies,

programming_language: TypeScript
repo_url: https://github.com/nextauthjs/next-auth
commit_details: repo_url: https://github.com/nextauthjs/next-auth
author: Balázs Orbán
author_date: 2022-06-20 10:38:21+02:00
msg: test: add test for invalid `callbackUrl` handling
repository: repo_url: https://github.com/nextauthjs/next-auth
repo_name: nextauthjs/next-auth
description: Authentication for the Web.
repo_language: TypeScript
owner: nextauthjs
file_changes: filename: assert.test.ts
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,22 @@ it("Show error page if secret is not defined", async () => {
   expect(log.error).toBeCalledWith("NO_SECRET", expect.anything())
 })
 
+it("Should show configuration error page on invalid `callbackUrl`", async () => {
+  const { res, log } = await handler(
+    { providers: [] },
+    { prod: true, params: { callbackUrl: "invalid-callback" } }
+  )
+
+  expect(res.status).toBe(500)
+  expect(res.html).toMatch(/there is a problem with the server configuration./i)
+  expect(res.html).toMatch(/check the server logs for more information./i)
+
+  expect(log.error).toBeCalledWith(
+    "INVALID_CALLBACK_URL_ERROR",
+    expect.anything()
+  )
+})
+
 it("Allow relative `callbackUrl`", async () => {
   const { res, log } = await handler(
     { providers: [] },

programming_language: TypeScript