Security context
High· 7.5GHSA-25mp-g6fv-mqxx CVE-2021-43803CWE-20Published Dec 7, 2021

Unexpected server crash in Next.js.

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

12.0.0 → fixed in 12.0.50.9.9 → fixed in 11.1.3

Details

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. Note that prior version 0.9.9 package `next` hosted a different utility (0.4.1 being the latest version of that codebase), and this advisory does not apply to those versions.

The fix

Fix server be killed by handling unhandled promise with

chentsulin· Dec 3, 2021, 11:05 AM+1561559880233357
packages/next/server/next-server.ts+156 155
@@ -359,198 +359,199 @@ 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)
+ }
- addRequestMeta(req, '__NEXT_INIT_URL', req.url)
- addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })
+ addRequestMeta(req, '__NEXT_INIT_URL', req.url)
+ 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)
}
- } else {
- params = utils.dynamicRouteMatcher!(matchedPathnameNoExt)
- }
- if (params) {
- params = utils.normalizeDynamicRouteParams(params).params
+ if (params) {
+ params = utils.normalizeDynamicRouteParams(params).params
- matchedPathname = utils.interpolateDynamicPath(
- matchedPathname,
- params
- )
- req.url = utils.interpolateDynamicPath(req.url!, params)
- }
+ matchedPathname = utils.interpolateDynamicPath(
+ matchedPathname,
+ params
+ )
+ req.url = utils.interpolateDynamicPath(req.url!, params)
+ }
- if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
- req.url = formatUrl({
- ...parsedPath,
- pathname: matchedPathname,
- })
- }
+ if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
+ req.url = formatUrl({
+ ...parsedPath,
+ pathname: matchedPathname,
+ })
+ }
- 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', {})
+ 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
- res.statusCode = 200
- try {
return await this.run(req, res, parsedUrl)
} catch (err) {
if (this.minimalMode || this.renderOpts.dev) {

Ensure invalid URLs respond with 400 correctly (#32092)

JJ Kasper· Dec 3, 2021, 05:31 PM+1981636d98b4fb43
packages/next/build/swc/index.js+1 1
@@ -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
@@ -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
@@ -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')

References