Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure invalid URLs respond with 400 correctly #32092

Merged
merged 3 commits into from Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
322 changes: 165 additions & 157 deletions packages/next/server/next-server.ts
Expand Up @@ -359,200 +359,208 @@ 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)
}

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
}
Expand Down