CVE-2022-31093: Improper Check for Unusual or Exceptional Conditions (CWE-754)
value: 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.
CWE 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.
CVSS v3 base score: 7.5
Published: 2022-06-27T22:15Z
Repository: https://github.com/nextauthjs/next-auth
Language: TypeScript
Files changed: docs/docs/errors.md, packages/next-auth/src/core/errors.ts, packages/next-auth/src/core/index.ts, packages/next-auth/src/core/lib/assert.ts, packages/next-auth/src/core/lib/cookie.ts
Commit date: 2022-06-10 13:52:00 +0200
Commit message: fix: handle invalid `callbackUrl`

Diff: diff --git a/docs/docs/errors.md b/docs/docs/errors.md
index f9f99d1..076c2b7 100644
--- a/docs/docs/errors.md
+++ b/docs/docs/errors.md
@@ -97,11 +97,11 @@ This is required to store the verification token. Please see the [email provider
 
 #### CALLBACK_CREDENTIALS_JWT_ERROR
 
 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.
 
 In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
 
@@ -117,17 +117,21 @@ Check if `cookies.pkceCodeVerifier` is configured correctly.
 
 The default `code_challenge_method` is `"S256"`. This is currently not configurable to `"plain"`, [as per RFC7636](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2): 
 > 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
 
 ```
   jose newkey -s 512 -t oct -a HS512
@@ -159,10 +163,10 @@ This error happens when `[...nextauth].js` file is not found inside `pages/api/a
 
 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
 
 This error might happen with some of the providers. It happens due to `openid-client`(which is peer dependency) node version mismatch. For instance, `openid-client` requires `>=14.2.0` for `lts/fermium` and has similar limits for the other versions. For the full list of the compatible node versions please see [package.json](https://github.com/panva/node-openid-client/blob/2a84e46992e1ebeaf685c3f87b65663d126e81aa/package.json#L78)
diff --git a/packages/next-auth/src/core/errors.ts b/packages/next-auth/src/core/errors.ts
index 84a4525..1270da9 100644
--- a/packages/next-auth/src/core/errors.ts
+++ b/packages/next-auth/src/core/errors.ts
@@ -61,10 +61,15 @@ export class MissingAdapter extends UnknownError {
 export class UnsupportedStrategy extends UnknownError {
   name = "UnsupportedStrategyError"
   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) {
   return s.replace(/([A-Z])/g, "_$1").toUpperCase()
 }
diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts
index 933e7ac..ab73c0f 100644
--- a/packages/next-auth/src/core/index.ts
+++ b/packages/next-auth/src/core/index.ts
@@ -106,11 +106,12 @@ export async function NextAuthHandler<
       case "signin":
         if (pages.signIn) {
           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 }
         }
 
         return render.signin()
       case "signout":
diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts
index 597c232..8f5a3f3 100644
--- a/packages/next-auth/src/core/lib/assert.ts
+++ b/packages/next-auth/src/core/lib/assert.ts
@@ -2,11 +2,14 @@ import {
   MissingAdapter,
   MissingAPIRoute,
   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"
 
 type ConfigError =
@@ -16,10 +19,18 @@ type ConfigError =
   | MissingAuthorize
   | MissingAdapter
 
 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.
  *
  * REVIEW: Make some of these and corresponding docs less Next.js specific?
@@ -42,12 +53,34 @@ export function assertConfig(
     } else {
       return "NO_SECRET"
     }
   }
 
+  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
 
   for (const provider of options.providers) {
     if (provider.type === "credentials") hasCredentials = true
diff --git a/packages/next-auth/src/core/lib/cookie.ts b/packages/next-auth/src/core/lib/cookie.ts
index 8cb6532..1805121 100644
--- a/packages/next-auth/src/core/lib/cookie.ts
+++ b/packages/next-auth/src/core/lib/cookie.ts
@@ -66,10 +66,11 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
       },
     },
     callbackUrl: {
       name: `${cookiePrefix}next-auth.callback-url`,
       options: {
+        httpOnly: true,
         sameSite: "lax",
         path: "/",
         secure: useSecureCookies,
       },
     },

Vulnerable code: 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.
https://next-auth.js.org/errors#jwt_session_error JWKKeySupport: the key does not support HS512 verify algorithm
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)
          if (error) signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
Fixed code: 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.
#### 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.

JWKKeySupport: the key does not support HS512 verify algorithm
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)
export class InvalidCallbackUrl extends UnknownError {
  name = "InvalidCallbackUrl"
  code = "INVALID_CALLBACK_URL_ERROR"
}

          if (error)
            signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
  InvalidCallbackUrl,
import parseUrl from "../../lib/parse-url"
import { defaultCookies } from "./cookie"
function isValidHttpUrl(url: string) {
  try {
    return /^https?:/.test(new URL(url).protocol)
  } catch {
    return false
  }
}

  const callbackUrlParam = req.query?.callbackUrl as string | undefined

  if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam)) {
    return new InvalidCallbackUrl(
      `Invalid callback URL. Received: ${callbackUrlParam}`
    )
  }

  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}`
    )
  }

        httpOnly: true,