VYPR
High severityNVD Advisory· Published Dec 9, 2021· Updated Aug 4, 2024

Unexpected server crash in Next.js

CVE-2021-43803

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nextnpm
>= 12.0.0, < 12.0.512.0.5
nextnpm
>= 0.9.9, < 11.1.311.1.3

Affected products

1

Patches

1
6d98b4fb4315

Ensure invalid URLs respond with 400 correctly (#32092)

https://github.com/vercel/next.jsJJ KasperDec 3, 2021via ghsa
3 files changed · +198 163
  • packages/next/build/swc/index.js+1 1 modified
    @@ -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()
    
  • packages/next/server/next-server.ts+170 162 modified
    @@ -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
           }
    
  • test/integration/production/test/security.js+27 0 modified
    @@ -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')
     
    

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.