CVE-2021-43803 (published 2021-12-10T00:15Z)

Next.js is a React framework. In versions of Next.js prior to 12.0.5 or 11.1.3, invalid or malformed URLs could lead to a server crash. In order to be affected by this issue, the deployment must use Next.js versions above 11.1.0 and below 12.0.5, Node.js above 15.0.0, and next start or a custom server. Deployments on Vercel are not affected, along with similar environments where invalid requests are filtered before reaching Next.js. Versions 12.0.5 and 11.1.3 contain patches for this issue.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/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: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/vercel/next.js
commit_details: repo_url: https://github.com/vercel/next.js
author: JJ Kasper
author_date: 2021-12-03 11:31:09-06:00
msg: Ensure invalid URLs respond with 400 correctly (#32092)

This ensures we catch any errors in `handleRequest` so that we can respond with a 400 for invalid requests. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

Fixes: https://github.com/vercel/next.js/issues/32075
Closes: https://github.com/vercel/next.js/pull/32080
repository: repo_url: https://github.com/vercel/next.js
repo_name: vercel/next.js
description: The React Framework
repo_language: JavaScript
owner: vercel
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 import { platform, arch } from 'os'
 import { platformArchTriples } from '@napi-rs/triples'
-import Log from '../output/log'
+import * as Log from '../output/log'
 
 const ArchName = arch()
 const PlatformName = platform()

programming_language: JavaScript
filename: next-server.ts
change_type: ModificationType.MODIFY
diff: @@ -359,206 +359,214 @@ export default class Server {
     res: ServerResponse,
     parsedUrl?: NextUrlWithParsedQuery
   ): Promise<void> {
-    const urlParts = (req.url || '').split('?')
-    const urlNoQuery = urlParts[0]
-
-    if (urlNoQuery?.match(/(\\|\/\/)/)) {
-      const cleanUrl = normalizeRepeatedSlashes(req.url!)
-      res.setHeader('Location', cleanUrl)
-      res.setHeader('Refresh', `0;url=${cleanUrl}`)
-      res.statusCode = 308
-      res.end(cleanUrl)
-      return
-    }
+    try {
+      const urlParts = (req.url || '').split('?')
+      const urlNoQuery = urlParts[0]
+
+      if (urlNoQuery?.match(/(\\|\/\/)/)) {
+        const cleanUrl = normalizeRepeatedSlashes(req.url!)
+        res.setHeader('Location', cleanUrl)
+        res.setHeader('Refresh', `0;url=${cleanUrl}`)
+        res.statusCode = 308
+        res.end(cleanUrl)
+        return
+      }
 
-    setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))
+      setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))
 
-    // Parse url if parsedUrl not provided
-    if (!parsedUrl || typeof parsedUrl !== 'object') {
-      parsedUrl = parseUrl(req.url!, true)
-    }
+      // Parse url if parsedUrl not provided
+      if (!parsedUrl || typeof parsedUrl !== 'object') {
+        parsedUrl = parseUrl(req.url!, true)
+      }
 
-    // Parse the querystring ourselves if the user doesn't handle querystring parsing
-    if (typeof parsedUrl.query === 'string') {
-      parsedUrl.query = parseQs(parsedUrl.query)
-    }
+      // Parse the querystring ourselves if the user doesn't handle querystring parsing
+      if (typeof parsedUrl.query === 'string') {
+        parsedUrl.query = parseQs(parsedUrl.query)
+      }
 
-    // When there are hostname and port we build an absolute URL
-    const initUrl =
-      this.hostname && this.port
-        ? `http://${this.hostname}:${this.port}${req.url}`
-        : req.url
+      // When there are hostname and port we build an absolute URL
+      const initUrl =
+        this.hostname && this.port
+          ? `http://${this.hostname}:${this.port}${req.url}`
+          : req.url
 
-    addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
-    addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })
+      addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
+      addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })
 
-    const url = parseNextUrl({
-      headers: req.headers,
-      nextConfig: this.nextConfig,
-      url: req.url?.replace(/^\/+/, '/'),
-    })
+      const url = parseNextUrl({
+        headers: req.headers,
+        nextConfig: this.nextConfig,
+        url: req.url?.replace(/^\/+/, '/'),
+      })
 
-    if (url.basePath) {
-      req.url = replaceBasePath(req.url!, this.nextConfig.basePath)
-      addRequestMeta(req, '_nextHadBasePath', true)
-    }
+      if (url.basePath) {
+        req.url = replaceBasePath(req.url!, this.nextConfig.basePath)
+        addRequestMeta(req, '_nextHadBasePath', true)
+      }
 
-    if (
-      this.minimalMode &&
-      req.headers['x-matched-path'] &&
-      typeof req.headers['x-matched-path'] === 'string'
-    ) {
-      const reqUrlIsDataUrl = req.url?.includes('/_next/data')
-      const matchedPathIsDataUrl =
-        req.headers['x-matched-path']?.includes('/_next/data')
-      const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl
-
-      let parsedPath = parseUrl(
-        isDataUrl ? req.url! : (req.headers['x-matched-path'] as string),
-        true
-      )
+      if (
+        this.minimalMode &&
+        req.headers['x-matched-path'] &&
+        typeof req.headers['x-matched-path'] === 'string'
+      ) {
+        const reqUrlIsDataUrl = req.url?.includes('/_next/data')
+        const matchedPathIsDataUrl =
+          req.headers['x-matched-path']?.includes('/_next/data')
+        const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl
+
+        let parsedPath = parseUrl(
+          isDataUrl ? req.url! : (req.headers['x-matched-path'] as string),
+          true
+        )
 
-      let matchedPathname = parsedPath.pathname!
+        let matchedPathname = parsedPath.pathname!
 
-      let matchedPathnameNoExt = isDataUrl
-        ? matchedPathname.replace(/\.json$/, '')
-        : matchedPathname
+        let matchedPathnameNoExt = isDataUrl
+          ? matchedPathname.replace(/\.json$/, '')
+          : matchedPathname
 
-      if (this.nextConfig.i18n) {
-        const localePathResult = normalizeLocalePath(
-          matchedPathname || '/',
-          this.nextConfig.i18n.locales
-        )
+        if (this.nextConfig.i18n) {
+          const localePathResult = normalizeLocalePath(
+            matchedPathname || '/',
+            this.nextConfig.i18n.locales
+          )
 
-        if (localePathResult.detectedLocale) {
-          parsedUrl.query.__nextLocale = localePathResult.detectedLocale
+          if (localePathResult.detectedLocale) {
+            parsedUrl.query.__nextLocale = localePathResult.detectedLocale
+          }
         }
-      }
-
-      if (isDataUrl) {
-        matchedPathname = denormalizePagePath(matchedPathname)
-        matchedPathnameNoExt = denormalizePagePath(matchedPathnameNoExt)
-      }
 
-      const pageIsDynamic = isDynamicRoute(matchedPathnameNoExt)
-      const combinedRewrites: Rewrite[] = []
+        if (isDataUrl) {
+          matchedPathname = denormalizePagePath(matchedPathname)
+          matchedPathnameNoExt = denormalizePagePath(matchedPathnameNoExt)
+        }
 
-      combinedRewrites.push(...this.customRoutes.rewrites.beforeFiles)
-      combinedRewrites.push(...this.customRoutes.rewrites.afterFiles)
-      combinedRewrites.push(...this.customRoutes.rewrites.fallback)
+        const pageIsDynamic = isDynamicRoute(matchedPathnameNoExt)
+        const combinedRewrites: Rewrite[] = []
 
-      const utils = getUtils({
-        pageIsDynamic,
-        page: matchedPathnameNoExt,
-        i18n: this.nextConfig.i18n,
-        basePath: this.nextConfig.basePath,
-        rewrites: combinedRewrites,
-      })
+        combinedRewrites.push(...this.customRoutes.rewrites.beforeFiles)
+        combinedRewrites.push(...this.customRoutes.rewrites.afterFiles)
+        combinedRewrites.push(...this.customRoutes.rewrites.fallback)
 
-      try {
-        // ensure parsedUrl.pathname includes URL before processing
-        // rewrites or they won't match correctly
-        if (this.nextConfig.i18n && !url.locale?.path.detectedLocale) {
-          parsedUrl.pathname = `/${url.locale?.locale}${parsedUrl.pathname}`
-        }
-        utils.handleRewrites(req, parsedUrl)
+        const utils = getUtils({
+          pageIsDynamic,
+          page: matchedPathnameNoExt,
+          i18n: this.nextConfig.i18n,
+          basePath: this.nextConfig.basePath,
+          rewrites: combinedRewrites,
+        })
 
-        // interpolate dynamic params and normalize URL if needed
-        if (pageIsDynamic) {
-          let params: ParsedUrlQuery | false = {}
+        try {
+          // ensure parsedUrl.pathname includes URL before processing
+          // rewrites or they won't match correctly
+          if (this.nextConfig.i18n && !url.locale?.path.detectedLocale) {
+            parsedUrl.pathname = `/${url.locale?.locale}${parsedUrl.pathname}`
+          }
+          utils.handleRewrites(req, parsedUrl)
 
-          Object.assign(parsedUrl.query, parsedPath.query)
-          const paramsResult = utils.normalizeDynamicRouteParams(
-            parsedUrl.query
-          )
+          // interpolate dynamic params and normalize URL if needed
+          if (pageIsDynamic) {
+            let params: ParsedUrlQuery | false = {}
 
-          if (paramsResult.hasValidParams) {
-            params = paramsResult.params
-          } else if (req.headers['x-now-route-matches']) {
-            const opts: Record<string, string> = {}
-            params = utils.getParamsFromRouteMatches(
-              req,
-              opts,
-              parsedUrl.query.__nextLocale || ''
+            Object.assign(parsedUrl.query, parsedPath.query)
+            const paramsResult = utils.normalizeDynamicRouteParams(
+              parsedUrl.query
             )
 
-            if (opts.locale) {
-              parsedUrl.query.__nextLocale = opts.locale
+            if (paramsResult.hasValidParams) {
+              params = paramsResult.params
+            } else if (req.headers['x-now-route-matches']) {
+              const opts: Record<string, string> = {}
+              params = utils.getParamsFromRouteMatches(
+                req,
+                opts,
+                parsedUrl.query.__nextLocale || ''
+              )
+
+              if (opts.locale) {
+                parsedUrl.query.__nextLocale = opts.locale
+              }
+            } else {
+              params = utils.dynamicRouteMatcher!(matchedPathnameNoExt)
+            }
+
+            if (params) {
+              params = utils.normalizeDynamicRouteParams(params).params
+
+              matchedPathname = utils.interpolateDynamicPath(
+                matchedPathname,
+                params
+              )
+              req.url = utils.interpolateDynamicPath(req.url!, params)
             }
-          } else {
-            params = utils.dynamicRouteMatcher!(matchedPathnameNoExt)
-          }
 
-          if (params) {
-            params = utils.normalizeDynamicRouteParams(params).params
+            if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
+              req.url = formatUrl({
+                ...parsedPath,
+                pathname: matchedPathname,
+              })
+            }
 
-            matchedPathname = utils.interpolateDynamicPath(
-              matchedPathname,
-              params
-            )
-            req.url = utils.interpolateDynamicPath(req.url!, params)
+            Object.assign(parsedUrl.query, params)
+            utils.normalizeVercelUrl(req, true)
           }
-
-          if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
-            req.url = formatUrl({
-              ...parsedPath,
-              pathname: matchedPathname,
-            })
+        } catch (err) {
+          if (err instanceof DecodeError) {
+            res.statusCode = 400
+            return this.renderError(null, req, res, '/_error', {})
           }
-
-          Object.assign(parsedUrl.query, params)
-          utils.normalizeVercelUrl(req, true)
-        }
-      } catch (err) {
-        if (err instanceof DecodeError) {
-          res.statusCode = 400
-          return this.renderError(null, req, res, '/_error', {})
+          throw err
         }
-        throw err
-      }
 
-      parsedUrl.pathname = `${this.nextConfig.basePath || ''}${
-        matchedPathname === '/' && this.nextConfig.basePath
-          ? ''
-          : matchedPathname
-      }`
-      url.pathname = parsedUrl.pathname
-    }
+        parsedUrl.pathname = `${this.nextConfig.basePath || ''}${
+          matchedPathname === '/' && this.nextConfig.basePath
+            ? ''
+            : matchedPathname
+        }`
+        url.pathname = parsedUrl.pathname
+      }
 
-    addRequestMeta(req, '__nextHadTrailingSlash', url.locale?.trailingSlash)
-    if (url.locale?.domain) {
-      addRequestMeta(req, '__nextIsLocaleDomain', true)
-    }
+      addRequestMeta(req, '__nextHadTrailingSlash', url.locale?.trailingSlash)
+      if (url.locale?.domain) {
+        addRequestMeta(req, '__nextIsLocaleDomain', true)
+      }
 
-    if (url.locale?.path.detectedLocale) {
-      req.url = formatUrl(url)
-      addRequestMeta(req, '__nextStrippedLocale', true)
-      if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
-        return this.render404(req, res, parsedUrl)
+      if (url.locale?.path.detectedLocale) {
+        req.url = formatUrl(url)
+        addRequestMeta(req, '__nextStrippedLocale', true)
+        if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
+          return this.render404(req, res, parsedUrl)
+        }
       }
-    }
 
-    if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
-      if (url?.locale?.locale) {
-        parsedUrl.query.__nextLocale = url.locale.locale
+      if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
+        if (url?.locale?.locale) {
+          parsedUrl.query.__nextLocale = url.locale.locale
+        }
       }
-    }
 
-    if (url?.locale?.defaultLocale) {
-      parsedUrl.query.__nextDefaultLocale = url.locale.defaultLocale
-    }
+      if (url?.locale?.defaultLocale) {
+        parsedUrl.query.__nextDefaultLocale = url.locale.defaultLocale
+      }
 
-    if (url.locale?.redirect) {
-      res.setHeader('Location', url.locale.redirect)
-      res.statusCode = TEMPORARY_REDIRECT_STATUS
-      res.end()
-      return
-    }
+      if (url.locale?.redirect) {
+        res.setHeader('Location', url.locale.redirect)
+        res.statusCode = TEMPORARY_REDIRECT_STATUS
+        res.end()
+        return
+      }
 
-    res.statusCode = 200
-    try {
+      res.statusCode = 200
       return await this.run(req, res, parsedUrl)
-    } catch (err) {
+    } catch (err: any) {
+      if (
+        (err && typeof err === 'object' && err.code === 'ERR_INVALID_URL') ||
+        err instanceof DecodeError
+      ) {
+        res.statusCode = 400
+        return this.renderError(null, req, res, '/_error', {})
+      }
+
       if (this.minimalMode || this.renderOpts.dev) {
         throw err
       }

programming_language: TypeScript
filename: security.js
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@
 /* global browserName */
 import webdriver from 'next-webdriver'
 import { readFileSync } from 'fs'
+import http from 'http'
 import url from 'url'
 import { join } from 'path'
 import {
@@ -27,6 +28,32 @@ async function checkInjected(browser) {
 
 module.exports = (context) => {
   describe('With Security Related Issues', () => {
+    it('should handle invalid URL properly', async () => {
+      async function invalidRequest() {
+        return new Promise((resolve, reject) => {
+          const request = http.request(
+            {
+              hostname: `localhost`,
+              port: context.appPort,
+              path: `*`,
+            },
+            (response) => {
+              resolve(response.statusCode)
+            }
+          )
+          request.on('error', (err) => reject(err))
+          request.end()
+        })
+      }
+      try {
+        expect(await invalidRequest()).toBe(400)
+        expect(await invalidRequest()).toBe(400)
+      } catch (err) {
+        // eslint-disable-next-line
+        expect(err.code).toBe('ECONNREFUSED')
+      }
+    })
+
     it('should only access files inside .next directory', async () => {
       const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
 

programming_language: JavaScript