Skip to content

Commit

Permalink
Ensure invalid URLs respond with 400 correctly (#32092)
Browse files Browse the repository at this point in the history
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: #32080
  • Loading branch information
ijjk committed Dec 3, 2021
1 parent 1c199a5 commit 6d98b4f
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 163 deletions.
2 changes: 1 addition & 1 deletion packages/next/build/swc/index.js
@@ -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()
Expand Down
332 changes: 170 additions & 162 deletions packages/next/server/next-server.ts
Expand Up @@ -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
}
Expand Down

0 comments on commit 6d98b4f

Please sign in to comment.