Skip to content

Commit

Permalink
Make sure routeKeys are PCRE compliant (#14987)
Browse files Browse the repository at this point in the history
This adds additional checks against the routeKeys used to build the named regexes for dynamic routes to ensure they follow PCRE rules for named capture groups

x-ref: vercel/vercel#4813
  • Loading branch information
ijjk authored Jul 8, 2020
1 parent 731cfa4 commit 16590f7
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 17 deletions.
37 changes: 21 additions & 16 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fileExists } from '../lib/file-exists'
import { findPagesDir } from '../lib/find-pages-dir'
import loadCustomRoutes, {
getRedirectStatus,
normalizeRouteRegex,
Redirect,
RouteType,
} from '../lib/load-custom-routes'
Expand Down Expand Up @@ -273,7 +274,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
permanent: undefined,
}
: {}),
regex: routeRegex.source,
regex: normalizeRouteRegex(routeRegex.source),
}
}

Expand All @@ -291,7 +292,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
const routeRegex = getRouteRegex(page)
return {
page,
regex: routeRegex.re.source,
regex: normalizeRouteRegex(routeRegex.re.source),
routeKeys: routeRegex.routeKeys,
namedRegex: routeRegex.namedRegex,
}
Expand Down Expand Up @@ -611,23 +612,24 @@ export default async function build(dir: string, conf = null): Promise<void> {
if (isDynamicRoute(page)) {
const routeRegex = getRouteRegex(dataRoute.replace(/\.json$/, ''))

dataRouteRegex = routeRegex.re.source.replace(
/\(\?:\\\/\)\?\$$/,
'\\.json$'
dataRouteRegex = normalizeRouteRegex(
routeRegex.re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$')
)
namedDataRouteRegex = routeRegex.namedRegex!.replace(
/\(\?:\/\)\?\$$/,
'\\.json$'
)
routeKeys = routeRegex.routeKeys
} else {
dataRouteRegex = new RegExp(
`^${path.posix.join(
'/_next/data',
escapeStringRegexp(buildId),
`${pagePath}.json`
)}$`
).source
dataRouteRegex = normalizeRouteRegex(
new RegExp(
`^${path.posix.join(
'/_next/data',
escapeStringRegexp(buildId),
`${pagePath}.json`
)}$`
).source
)
}

return {
Expand Down Expand Up @@ -887,14 +889,17 @@ export default async function build(dir: string, conf = null): Promise<void> {
)

finalDynamicRoutes[tbdRoute] = {
routeRegex: getRouteRegex(tbdRoute).re.source,
routeRegex: normalizeRouteRegex(getRouteRegex(tbdRoute).re.source),
dataRoute,
fallback: ssgFallbackPages.has(tbdRoute)
? `${normalizedRoute}.html`
: false,
dataRouteRegex: getRouteRegex(
dataRoute.replace(/\.json$/, '')
).re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$'),
dataRouteRegex: normalizeRouteRegex(
getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace(
/\(\?:\\\/\)\?\$$/,
'\\.json$'
)
),
}
})
const prerenderManifest: PrerenderManifest = {
Expand Down
5 changes: 5 additions & 0 deletions packages/next/lib/load-custom-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export function getRedirectStatus(route: Redirect): number {
)
}

export function normalizeRouteRegex(regex: string) {
// clean up un-necessary escaping from regex.source which turns / into \\/
return regex.replace(/\\\//g, '/')
}

function checkRedirect(
route: Redirect
): { invalidParts: string[]; hadInvalidStatus: boolean } {
Expand Down
36 changes: 35 additions & 1 deletion packages/next/next-server/lib/router/utils/route-regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ export function getRouteRegex(
// dead code eliminate for browser since it's only needed
// while generating routes-manifest
if (typeof window === 'undefined') {
let routeKeyCharCode = 97
let routeKeyCharLength = 1

// builds a minimal routeKey using only a-z and minimal number of characters
const getSafeRouteKey = () => {
let routeKey = ''

for (let i = 0; i < routeKeyCharLength; i++) {
routeKey += String.fromCharCode(routeKeyCharCode)
routeKeyCharCode++

if (routeKeyCharCode > 122) {
routeKeyCharLength++
routeKeyCharCode = 97
}
}
return routeKey
}

const routeKeys: { [named: string]: string } = {}

let namedParameterizedRoute = segments
Expand All @@ -59,7 +78,22 @@ export function getRouteRegex(
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
// replace any non-word characters since they can break
// the named regex
const cleanedKey = key.replace(/\W/g, '')
let cleanedKey = key.replace(/\W/g, '')
let invalidKey = false

// check if the key is still invalid and fallback to using a known
// safe key
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true
}
if (!isNaN(parseInt(cleanedKey.substr(0, 1)))) {
invalidKey = true
}

if (invalidKey) {
cleanedKey = getSafeRouteKey()
}

routeKeys[cleanedKey] = key
return repeat
? optional
Expand Down
13 changes: 13 additions & 0 deletions test/integration/dynamic-routing/pages/b/[123].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const getServerSideProps = ({ params }) => {
console.log({ params })

return {
props: {
params,
},
}
}

export default function Page(props) {
return <p id="props">{JSON.stringify(props)}</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const getServerSideProps = ({ params }) => {
console.log({ params })

return {
props: {
params,
},
}
}

export default function Page(props) {
return <p id="props">{JSON.stringify(props)}</p>
}
40 changes: 40 additions & 0 deletions test/integration/dynamic-routing/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,30 @@ function runTests(dev) {
rewrites: [],
redirects: expect.arrayContaining([]),
dataRoutes: [
{
dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
buildId
)}\\/b\\/([^\\/]+?)\\.json$`,
namedDataRouteRegex: `^/_next/data/${escapeRegex(
buildId
)}/b/(?<a>[^/]+?)\\.json$`,
page: '/b/[123]',
routeKeys: {
a: '123',
},
},
{
dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
buildId
)}\\/c\\/([^\\/]+?)\\.json$`,
namedDataRouteRegex: `^/_next/data/${escapeRegex(
buildId
)}/c/(?<a>[^/]+?)\\.json$`,
page: '/c/[alongparamnameshouldbeallowedeventhoughweird]',
routeKeys: {
a: 'alongparamnameshouldbeallowedeventhoughweird',
},
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
buildId
Expand Down Expand Up @@ -596,6 +620,14 @@ function runTests(dev) {
},
],
dynamicRoutes: [
{
namedRegex: '^/b/(?<a>[^/]+?)(?:/)?$',
page: '/b/[123]',
regex: normalizeRegEx('^\\/b\\/([^\\/]+?)(?:\\/)?$'),
routeKeys: {
a: '123',
},
},
{
namedRegex: `^/blog/(?<name>[^/]+?)/comment/(?<id>[^/]+?)(?:/)?$`,
page: '/blog/[name]/comment/[id]',
Expand All @@ -607,6 +639,14 @@ function runTests(dev) {
id: 'id',
},
},
{
namedRegex: '^/c/(?<a>[^/]+?)(?:/)?$',
page: '/c/[alongparamnameshouldbeallowedeventhoughweird]',
regex: normalizeRegEx('^\\/c\\/([^\\/]+?)(?:\\/)?$'),
routeKeys: {
a: 'alongparamnameshouldbeallowedeventhoughweird',
},
},
{
namedRegex: '^/catchall\\-dash/(?<helloworld>.+?)(?:/)?$',
page: '/catchall-dash/[...hello-world]',
Expand Down

0 comments on commit 16590f7

Please sign in to comment.