Skip to content

Commit 4888713

Browse files
committed
Ensure invalid URLs respond with 400 correctly
1 parent 97456e8 commit 4888713

File tree

1 file changed

+150
-140
lines changed

1 file changed

+150
-140
lines changed

packages/next/server/next-server.ts

+150-140
Original file line numberDiff line numberDiff line change
@@ -311,182 +311,192 @@ export default class Server {
311311
res: ServerResponse,
312312
parsedUrl?: UrlWithParsedQuery
313313
): Promise<void> {
314-
const urlParts = (req.url || '').split('?')
315-
const urlNoQuery = urlParts[0]
316-
317-
if (urlNoQuery?.match(/(\\|\/\/)/)) {
318-
const cleanUrl = normalizeRepeatedSlashes(req.url!)
319-
res.setHeader('Location', cleanUrl)
320-
res.setHeader('Refresh', `0;url=${cleanUrl}`)
321-
res.statusCode = 308
322-
res.end(cleanUrl)
323-
return
324-
}
314+
try {
315+
const urlParts = (req.url || '').split('?')
316+
const urlNoQuery = urlParts[0]
317+
318+
if (urlNoQuery?.match(/(\\|\/\/)/)) {
319+
const cleanUrl = normalizeRepeatedSlashes(req.url!)
320+
res.setHeader('Location', cleanUrl)
321+
res.setHeader('Refresh', `0;url=${cleanUrl}`)
322+
res.statusCode = 308
323+
res.end(cleanUrl)
324+
return
325+
}
325326

326-
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))
327+
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))
327328

328-
// Parse url if parsedUrl not provided
329-
if (!parsedUrl || typeof parsedUrl !== 'object') {
330-
const url: any = req.url
331-
parsedUrl = parseUrl(url, true)
332-
}
333-
const { basePath, i18n } = this.nextConfig
329+
// Parse url if parsedUrl not provided
330+
if (!parsedUrl || typeof parsedUrl !== 'object') {
331+
const url: any = req.url
332+
parsedUrl = parseUrl(url, true)
333+
}
334+
const { basePath, i18n } = this.nextConfig
334335

335-
// Parse the querystring ourselves if the user doesn't handle querystring parsing
336-
if (typeof parsedUrl.query === 'string') {
337-
parsedUrl.query = parseQs(parsedUrl.query)
338-
}
339-
;(req as any).__NEXT_INIT_QUERY = Object.assign({}, parsedUrl.query)
336+
// Parse the querystring ourselves if the user doesn't handle querystring parsing
337+
if (typeof parsedUrl.query === 'string') {
338+
parsedUrl.query = parseQs(parsedUrl.query)
339+
}
340+
;(req as any).__NEXT_INIT_QUERY = Object.assign({}, parsedUrl.query)
340341

341-
const url = parseNextUrl({
342-
headers: req.headers,
343-
nextConfig: this.nextConfig,
344-
url: req.url?.replace(/^\/+/, '/'),
345-
})
342+
const url = parseNextUrl({
343+
headers: req.headers,
344+
nextConfig: this.nextConfig,
345+
url: req.url?.replace(/^\/+/, '/'),
346+
})
346347

347-
if (url.basePath) {
348-
;(req as any)._nextHadBasePath = true
349-
req.url = req.url!.replace(basePath, '') || '/'
350-
}
348+
if (url.basePath) {
349+
;(req as any)._nextHadBasePath = true
350+
req.url = req.url!.replace(basePath, '') || '/'
351+
}
351352

352-
if (
353-
this.minimalMode &&
354-
req.headers['x-matched-path'] &&
355-
typeof req.headers['x-matched-path'] === 'string'
356-
) {
357-
const reqUrlIsDataUrl = req.url?.includes('/_next/data')
358-
const matchedPathIsDataUrl =
359-
req.headers['x-matched-path']?.includes('/_next/data')
360-
const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl
361-
362-
let parsedPath = parseUrl(
363-
isDataUrl ? req.url! : (req.headers['x-matched-path'] as string),
364-
true
365-
)
366-
const { pathname, query } = parsedPath
367-
let matchedPathname = pathname as string
353+
if (
354+
this.minimalMode &&
355+
req.headers['x-matched-path'] &&
356+
typeof req.headers['x-matched-path'] === 'string'
357+
) {
358+
const reqUrlIsDataUrl = req.url?.includes('/_next/data')
359+
const matchedPathIsDataUrl =
360+
req.headers['x-matched-path']?.includes('/_next/data')
361+
const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl
362+
363+
let parsedPath = parseUrl(
364+
isDataUrl ? req.url! : (req.headers['x-matched-path'] as string),
365+
true
366+
)
367+
const { pathname, query } = parsedPath
368+
let matchedPathname = pathname as string
368369

369-
let matchedPathnameNoExt = isDataUrl
370-
? matchedPathname.replace(/\.json$/, '')
371-
: matchedPathname
370+
let matchedPathnameNoExt = isDataUrl
371+
? matchedPathname.replace(/\.json$/, '')
372+
: matchedPathname
372373

373-
if (i18n) {
374-
const localePathResult = normalizeLocalePath(
375-
matchedPathname || '/',
376-
i18n.locales
377-
)
374+
if (i18n) {
375+
const localePathResult = normalizeLocalePath(
376+
matchedPathname || '/',
377+
i18n.locales
378+
)
378379

379-
if (localePathResult.detectedLocale) {
380-
parsedUrl.query.__nextLocale = localePathResult.detectedLocale
380+
if (localePathResult.detectedLocale) {
381+
parsedUrl.query.__nextLocale = localePathResult.detectedLocale
382+
}
381383
}
382-
}
383384

384-
if (isDataUrl) {
385-
matchedPathname = denormalizePagePath(matchedPathname)
386-
matchedPathnameNoExt = denormalizePagePath(matchedPathnameNoExt)
387-
}
388-
389-
const pageIsDynamic = isDynamicRoute(matchedPathnameNoExt)
390-
const combinedRewrites: Rewrite[] = []
385+
if (isDataUrl) {
386+
matchedPathname = denormalizePagePath(matchedPathname)
387+
matchedPathnameNoExt = denormalizePagePath(matchedPathnameNoExt)
388+
}
391389

392-
combinedRewrites.push(...this.customRoutes.rewrites.beforeFiles)
393-
combinedRewrites.push(...this.customRoutes.rewrites.afterFiles)
394-
combinedRewrites.push(...this.customRoutes.rewrites.fallback)
390+
const pageIsDynamic = isDynamicRoute(matchedPathnameNoExt)
391+
const combinedRewrites: Rewrite[] = []
395392

396-
const utils = getUtils({
397-
pageIsDynamic,
398-
page: matchedPathnameNoExt,
399-
i18n: this.nextConfig.i18n,
400-
basePath: this.nextConfig.basePath,
401-
rewrites: combinedRewrites,
402-
})
393+
combinedRewrites.push(...this.customRoutes.rewrites.beforeFiles)
394+
combinedRewrites.push(...this.customRoutes.rewrites.afterFiles)
395+
combinedRewrites.push(...this.customRoutes.rewrites.fallback)
403396

404-
utils.handleRewrites(req, parsedUrl)
397+
const utils = getUtils({
398+
pageIsDynamic,
399+
page: matchedPathnameNoExt,
400+
i18n: this.nextConfig.i18n,
401+
basePath: this.nextConfig.basePath,
402+
rewrites: combinedRewrites,
403+
})
405404

406-
// interpolate dynamic params and normalize URL if needed
407-
if (pageIsDynamic) {
408-
let params: ParsedUrlQuery | false = {}
405+
utils.handleRewrites(req, parsedUrl)
409406

410-
Object.assign(parsedUrl.query, query)
411-
const paramsResult = utils.normalizeDynamicRouteParams(parsedUrl.query)
407+
// interpolate dynamic params and normalize URL if needed
408+
if (pageIsDynamic) {
409+
let params: ParsedUrlQuery | false = {}
412410

413-
if (paramsResult.hasValidParams) {
414-
params = paramsResult.params
415-
} else if (req.headers['x-now-route-matches']) {
416-
const opts: Record<string, string> = {}
417-
params = utils.getParamsFromRouteMatches(
418-
req,
419-
opts,
420-
(parsedUrl.query.__nextLocale as string | undefined) || ''
411+
Object.assign(parsedUrl.query, query)
412+
const paramsResult = utils.normalizeDynamicRouteParams(
413+
parsedUrl.query
421414
)
422415

423-
if (opts.locale) {
424-
parsedUrl.query.__nextLocale = opts.locale
416+
if (paramsResult.hasValidParams) {
417+
params = paramsResult.params
418+
} else if (req.headers['x-now-route-matches']) {
419+
const opts: Record<string, string> = {}
420+
params = utils.getParamsFromRouteMatches(
421+
req,
422+
opts,
423+
(parsedUrl.query.__nextLocale as string | undefined) || ''
424+
)
425+
426+
if (opts.locale) {
427+
parsedUrl.query.__nextLocale = opts.locale
428+
}
429+
} else {
430+
params = utils.dynamicRouteMatcher!(matchedPathnameNoExt)
425431
}
426-
} else {
427-
params = utils.dynamicRouteMatcher!(matchedPathnameNoExt)
428-
}
429432

430-
if (params) {
431-
params = utils.normalizeDynamicRouteParams(params).params
433+
if (params) {
434+
params = utils.normalizeDynamicRouteParams(params).params
432435

433-
matchedPathname = utils.interpolateDynamicPath(
434-
matchedPathname,
435-
params
436-
)
437-
req.url = utils.interpolateDynamicPath(req.url!, params)
438-
}
436+
matchedPathname = utils.interpolateDynamicPath(
437+
matchedPathname,
438+
params
439+
)
440+
req.url = utils.interpolateDynamicPath(req.url!, params)
441+
}
439442

440-
if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
441-
req.url = formatUrl({
442-
...parsedPath,
443-
pathname: matchedPathname,
444-
})
443+
if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
444+
req.url = formatUrl({
445+
...parsedPath,
446+
pathname: matchedPathname,
447+
})
448+
}
449+
450+
Object.assign(parsedUrl.query, params)
451+
utils.normalizeVercelUrl(req, true)
445452
}
446453

447-
Object.assign(parsedUrl.query, params)
448-
utils.normalizeVercelUrl(req, true)
454+
parsedUrl.pathname = `${basePath || ''}${
455+
matchedPathname === '/' && basePath ? '' : matchedPathname
456+
}`
449457
}
450458

451-
parsedUrl.pathname = `${basePath || ''}${
452-
matchedPathname === '/' && basePath ? '' : matchedPathname
453-
}`
454-
}
455-
456-
;(req as any).__nextHadTrailingSlash = url.locale?.trailingSlash
457-
if (url.locale?.domain) {
458-
;(req as any).__nextIsLocaleDomain = true
459-
}
459+
;(req as any).__nextHadTrailingSlash = url.locale?.trailingSlash
460+
if (url.locale?.domain) {
461+
;(req as any).__nextIsLocaleDomain = true
462+
}
460463

461-
if (url.locale?.path.detectedLocale) {
462-
req.url = formatUrl(url)
463-
;(req as any).__nextStrippedLocale = true
464-
if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
465-
return this.render404(req, res, parsedUrl)
464+
if (url.locale?.path.detectedLocale) {
465+
req.url = formatUrl(url)
466+
;(req as any).__nextStrippedLocale = true
467+
if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
468+
return this.render404(req, res, parsedUrl)
469+
}
466470
}
467-
}
468471

469-
if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
470-
if (url?.locale?.locale) {
471-
parsedUrl.query.__nextLocale = url.locale.locale
472+
if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
473+
if (url?.locale?.locale) {
474+
parsedUrl.query.__nextLocale = url.locale.locale
475+
}
472476
}
473-
}
474477

475-
if (url?.locale?.defaultLocale) {
476-
parsedUrl.query.__nextDefaultLocale = url.locale.defaultLocale
477-
}
478+
if (url?.locale?.defaultLocale) {
479+
parsedUrl.query.__nextDefaultLocale = url.locale.defaultLocale
480+
}
478481

479-
if (url.locale?.redirect) {
480-
res.setHeader('Location', url.locale.redirect)
481-
res.statusCode = TEMPORARY_REDIRECT_STATUS
482-
res.end()
483-
return
484-
}
482+
if (url.locale?.redirect) {
483+
res.setHeader('Location', url.locale.redirect)
484+
res.statusCode = TEMPORARY_REDIRECT_STATUS
485+
res.end()
486+
return
487+
}
485488

486-
res.statusCode = 200
487-
try {
489+
res.statusCode = 200
488490
return await this.run(req, res, parsedUrl)
489491
} catch (err) {
492+
if (
493+
(err && typeof err === 'object' && err.code === 'ERR_INVALID_URL') ||
494+
err instanceof DecodeError
495+
) {
496+
res.statusCode = 400
497+
return this.renderError(null, req, res, '/_error', {})
498+
}
499+
490500
if (this.minimalMode || this.renderOpts.dev) {
491501
throw err
492502
}

0 commit comments

Comments
 (0)