Skip to content

Commit dadf7e9

Browse files
authored
fix(router-core): null prototype input/output objects (#6882)
1 parent d306d58 commit dadf7e9

File tree

11 files changed

+121
-33
lines changed

11 files changed

+121
-33
lines changed

packages/react-router/tests/navigate.test.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,17 +479,26 @@ describe('router.navigate navigation using layout routes resolves correctly', ()
479479
await router.load()
480480

481481
expect(router.state.location.pathname).toBe('/search')
482-
expect(router.state.location.search).toStrictEqual({ 'foo=bar': 2 })
482+
expect(router.state.location.search).toStrictEqual(
483+
toNullObj({ 'foo=bar': 2 }),
484+
)
483485

484486
await router.navigate({
485487
search: { 'foo=bar': 3 },
486488
} as any)
487489
await router.invalidate()
488490

489-
expect(router.state.location.search).toStrictEqual({ 'foo=bar': 3 })
491+
expect(router.state.location.search).toStrictEqual(
492+
toNullObj({ 'foo=bar': 3 }),
493+
)
490494
})
491495
})
492496

497+
function toNullObj<T>(obj: T): T {
498+
if (typeof obj === 'object') return Object.assign(Object.create(null), obj)
499+
return obj
500+
}
501+
493502
describe('relative navigation', () => {
494503
it('should navigate to a child route', async () => {
495504
const { router } = createTestRouter(

packages/router-core/src/new-process-route-tree.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,7 @@ function extractParams<T extends RouteLike>(
896896
): [rawParams: Record<string, string>, state: ParamExtractionState] {
897897
const list = buildBranch(leaf.node)
898898
let nodeParts: Array<string> | null = null
899-
const rawParams: Record<string, string> = {}
899+
const rawParams: Record<string, string> = Object.create(null)
900900
/** which segment of the path we're currently processing */
901901
let partIndex = leaf.extract?.part ?? 0
902902
/** which node of the route tree branch we're currently processing */
@@ -1330,8 +1330,8 @@ function getNodeMatch<T extends RouteLike>(
13301330
sliceIndex += parts[i]!.length
13311331
}
13321332
const splat = sliceIndex === path.length ? '/' : path.slice(sliceIndex)
1333-
bestFuzzy.rawParams ??= {}
1334-
bestFuzzy.rawParams['**'] = decodeURIComponent(splat)
1333+
bestFuzzy.rawParams ??= Object.create(null)
1334+
bestFuzzy.rawParams!['**'] = decodeURIComponent(splat)
13351335
return bestFuzzy
13361336
}
13371337

@@ -1348,7 +1348,11 @@ function validateMatchParams<T extends RouteLike>(
13481348
frame.rawParams = rawParams
13491349
frame.extract = state
13501350
const parsed = frame.node.parse!(rawParams)
1351-
frame.parsedParams = Object.assign({}, frame.parsedParams, parsed)
1351+
frame.parsedParams = Object.assign(
1352+
Object.create(null),
1353+
frame.parsedParams,
1354+
parsed,
1355+
)
13521356
return true
13531357
} catch {
13541358
return null

packages/router-core/src/path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export function interpolatePath({
279279
// Tracking if any params are missing in the `params` object
280280
// when interpolating the path
281281
let isMissingParams = false
282-
const usedParams: Record<string, unknown> = {}
282+
const usedParams: Record<string, unknown> = Object.create(null)
283283

284284
if (!path || path === '/')
285285
return { interpolatedPath: '/', usedParams, isMissingParams }

packages/router-core/src/qss.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function toValue(str: unknown) {
6464
export function decode(str: any): any {
6565
const searchParams = new URLSearchParams(str)
6666

67-
const result: Record<string, unknown> = {}
67+
const result: Record<string, unknown> = Object.create(null)
6868

6969
for (const [key, value] of searchParams.entries()) {
7070
const previousValue = result[key]

packages/router-core/src/router.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
functionalUpdate,
1313
isDangerousProtocol,
1414
last,
15+
nullReplaceEqualDeep,
1516
replaceEqualDeep,
1617
} from './utils'
1718
import {
@@ -1295,7 +1296,7 @@ export class RouterCore<
12951296
pathname: decodePath(pathname).path,
12961297
external: false,
12971298
searchStr,
1298-
search: replaceEqualDeep(
1299+
search: nullReplaceEqualDeep(
12991300
previousLocation?.search,
13001301
parsedSearch,
13011302
) as any,
@@ -1324,7 +1325,10 @@ export class RouterCore<
13241325
pathname: decodePath(url.pathname).path,
13251326
external: !!this.rewrite && url.origin !== this.origin,
13261327
searchStr,
1327-
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1328+
search: nullReplaceEqualDeep(
1329+
previousLocation?.search,
1330+
parsedSearch,
1331+
) as any,
13281332
hash: decodePath(url.hash.slice(1)).path,
13291333
state: replaceEqualDeep(previousLocation?.state, state),
13301334
}
@@ -1549,8 +1553,8 @@ export class RouterCore<
15491553
params: previousMatch?.params ?? routeParams,
15501554
_strictParams: strictParams,
15511555
search: previousMatch
1552-
? replaceEqualDeep(previousMatch.search, preMatchSearch)
1553-
: replaceEqualDeep(existingMatch.search, preMatchSearch),
1556+
? nullReplaceEqualDeep(previousMatch.search, preMatchSearch)
1557+
: nullReplaceEqualDeep(existingMatch.search, preMatchSearch),
15541558
_strictSearch: strictMatchSearch,
15551559
}
15561560
} else {
@@ -1572,7 +1576,7 @@ export class RouterCore<
15721576
pathname: interpolatedPath,
15731577
updatedAt: Date.now(),
15741578
search: previousMatch
1575-
? replaceEqualDeep(previousMatch.search, preMatchSearch)
1579+
? nullReplaceEqualDeep(previousMatch.search, preMatchSearch)
15761580
: preMatchSearch,
15771581
_strictSearch: strictMatchSearch,
15781582
searchError: undefined,
@@ -1630,7 +1634,7 @@ export class RouterCore<
16301634
// Update the match's params
16311635
const previousMatch = previousMatchesByRouteId.get(match.routeId)
16321636
match.params = previousMatch
1633-
? replaceEqualDeep(previousMatch.params, routeParams)
1637+
? nullReplaceEqualDeep(previousMatch.params, routeParams)
16341638
: routeParams
16351639

16361640
if (!existingMatch) {
@@ -1729,7 +1733,10 @@ export class RouterCore<
17291733
params = lastStateMatch.params
17301734
} else {
17311735
// Parse params through the route chain
1732-
const strictParams: Record<string, unknown> = { ...routeParams }
1736+
const strictParams: Record<string, unknown> = Object.assign(
1737+
Object.create(null),
1738+
routeParams,
1739+
)
17331740
for (const route of matchedRoutes) {
17341741
try {
17351742
extractStrictParams(
@@ -1836,7 +1843,10 @@ export class RouterCore<
18361843
// From search should always use the current location
18371844
const fromSearch = lightweightResult.search
18381845
// Same with params. It can't hurt to provide as many as possible
1839-
const fromParams = { ...lightweightResult.params }
1846+
const fromParams = Object.assign(
1847+
Object.create(null),
1848+
lightweightResult.params,
1849+
)
18401850

18411851
// Resolve the next to
18421852
// ensure this includes the basePath if set
@@ -1847,7 +1857,7 @@ export class RouterCore<
18471857
// Resolve the next params
18481858
const nextParams =
18491859
dest.params === false || dest.params === null
1850-
? {}
1860+
? Object.create(null)
18511861
: (dest.params ?? true) === true
18521862
? fromParams
18531863
: Object.assign(
@@ -1933,7 +1943,7 @@ export class RouterCore<
19331943
})
19341944

19351945
// Replace the equal deep
1936-
nextSearch = replaceEqualDeep(fromSearch, nextSearch)
1946+
nextSearch = nullReplaceEqualDeep(fromSearch, nextSearch)
19371947

19381948
// Stringify the next search
19391949
const searchStr = this.options.stringifySearch(nextSearch)
@@ -2013,7 +2023,7 @@ export class RouterCore<
20132023
let maskedNext = maskedDest ? build(maskedDest) : undefined
20142024

20152025
if (!maskedNext) {
2016-
const params = {}
2026+
const params = Object.create(null)
20172027

20182028
if (this.options.routeMasks) {
20192029
const match = findFlatMatch<RouteMask<TRouteTree>>(
@@ -2032,7 +2042,7 @@ export class RouterCore<
20322042
// Otherwise, use the matched params or the provided params value
20332043
const nextParams =
20342044
maskParams === false || maskParams === null
2035-
? {}
2045+
? Object.create(null)
20362046
: (maskParams ?? true) === true
20372047
? params
20382048
: Object.assign(params, functionalUpdate(maskParams, params))
@@ -3013,7 +3023,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
30133023
routesById: Record<string, TRouteLike>
30143024
processedTree: ProcessedTree<any, any, any>
30153025
}) {
3016-
const routeParams: Record<string, string> = {}
3026+
const routeParams: Record<string, string> = Object.create(null)
30173027
const trimmedPath = trimPathRight(pathname)
30183028

30193029
let foundRoute: TRouteLike | undefined = undefined
@@ -3022,7 +3032,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
30223032
if (match) {
30233033
foundRoute = match.route
30243034
Object.assign(routeParams, match.rawParams) // Copy params, because they're cached
3025-
parsedParams = Object.assign({}, match.parsedParams)
3035+
parsedParams = Object.assign(Object.create(null), match.parsedParams)
30263036
}
30273037

30283038
const matchedRoutes = match?.branch || [routesById[rootRouteId]!]

packages/router-core/src/utils.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,22 @@ export function functionalUpdate<TPrevious, TResult = TPrevious>(
215215
const hasOwn = Object.prototype.hasOwnProperty
216216
const isEnumerable = Object.prototype.propertyIsEnumerable
217217

218+
const createNull = () => Object.create(null)
219+
export const nullReplaceEqualDeep: typeof replaceEqualDeep = (prev, next) =>
220+
replaceEqualDeep(prev, next, createNull)
221+
218222
/**
219223
* This function returns `prev` if `_next` is deeply equal.
220224
* If not, it will replace any deeply equal children of `b` with those of `a`.
221225
* This can be used for structural sharing between immutable JSON values for example.
222226
* Do not use this with signals
223227
*/
224-
export function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {
228+
export function replaceEqualDeep<T>(
229+
prev: any,
230+
_next: T,
231+
_makeObj = () => ({}),
232+
_depth = 0,
233+
): T {
225234
if (isServer) {
226235
return _next
227236
}
@@ -243,7 +252,7 @@ export function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {
243252
if (!nextItems) return next
244253
const prevSize = prevItems.length
245254
const nextSize = nextItems.length
246-
const copy: any = array ? new Array(nextSize) : {}
255+
const copy: any = array ? new Array(nextSize) : _makeObj()
247256

248257
let equalItems = 0
249258

@@ -268,7 +277,7 @@ export function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {
268277
continue
269278
}
270279

271-
const v = replaceEqualDeep(p, n, _depth + 1)
280+
const v = replaceEqualDeep(p, n, _makeObj, _depth + 1)
272281
copy[key] = v
273282
if (v === p) equalItems++
274283
}

packages/router-core/tests/load.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ describe('redirect resolution', () => {
4646
expect(resolved.headers.get('Location')).toBe('/foo')
4747
expect(resolved.options.href).toBe('/foo')
4848
})
49+
50+
test.each(['/$a', '/$toString', '/$__proto__'])(
51+
'server startup redirects initial path %s to /undefined',
52+
async (initialPath) => {
53+
const rootRoute = new BaseRootRoute({})
54+
const slugRoute = new BaseRoute({
55+
getParentRoute: () => rootRoute,
56+
path: '/$slug',
57+
})
58+
59+
const routeTree = rootRoute.addChildren([slugRoute])
60+
61+
const router = new RouterCore({
62+
routeTree,
63+
history: createMemoryHistory({ initialEntries: [initialPath] }),
64+
isServer: true,
65+
})
66+
67+
await router.load()
68+
69+
expect(router.state.redirect).toEqual(
70+
expect.objectContaining({
71+
options: expect.objectContaining({ href: '/undefined' }),
72+
}),
73+
)
74+
expect(router.state.redirect?.headers.get('Location')).toBe('/undefined')
75+
},
76+
)
4977
})
5078

5179
describe('beforeLoad skip or exec', () => {

packages/router-core/tests/optional-path-params.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ describe('Optional Path Parameters', () => {
465465
},
466466
])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
467467
expect(matchPathname(input, matchingOptions)).toStrictEqual(
468-
expectedMatchedParams,
468+
toNullObj(expectedMatchedParams),
469469
)
470470
})
471471
})
@@ -527,3 +527,8 @@ describe('Optional Path Parameters', () => {
527527
})
528528
})
529529
})
530+
531+
function toNullObj<T>(obj: T): T {
532+
if (typeof obj === 'object') return Object.assign(Object.create(null), obj)
533+
return obj
534+
}

packages/router-core/tests/path.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ describe('matchPathname', () => {
736736
},
737737
])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
738738
expect(matchPathname(input, matchingOptions)).toStrictEqual(
739-
expectedMatchedParams,
739+
toNullObj(expectedMatchedParams),
740740
)
741741
})
742742
})
@@ -800,7 +800,7 @@ describe('matchPathname', () => {
800800
},
801801
])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
802802
expect(matchPathname(input, matchingOptions)).toStrictEqual(
803-
expectedMatchedParams,
803+
toNullObj(expectedMatchedParams),
804804
)
805805
})
806806
})
@@ -879,7 +879,7 @@ describe('matchPathname', () => {
879879
},
880880
])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
881881
expect(matchPathname(input, matchingOptions)).toStrictEqual(
882-
expectedMatchedParams,
882+
toNullObj(expectedMatchedParams),
883883
)
884884
})
885885
})
@@ -1217,3 +1217,8 @@ describe('parsePathname', () => {
12171217
})
12181218
})
12191219
})
1220+
1221+
function toNullObj<T>(obj: T): T {
1222+
if (typeof obj === 'object') return Object.assign(Object.create(null), obj)
1223+
return obj
1224+
}

packages/solid-router/tests/navigate.test.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,17 +480,26 @@ describe('router.navigate navigation using layout routes resolves correctly', ()
480480
await router.load()
481481

482482
expect(router.state.location.pathname).toBe('/search')
483-
expect(router.state.location.search).toStrictEqual({ 'foo=bar': 2 })
483+
expect(router.state.location.search).toStrictEqual(
484+
toNullObj({ 'foo=bar': 2 }),
485+
)
484486

485487
await router.navigate({
486488
search: { 'foo=bar': 3 },
487489
} as any)
488490
await router.invalidate()
489491

490-
expect(router.state.location.search).toStrictEqual({ 'foo=bar': 3 })
492+
expect(router.state.location.search).toStrictEqual(
493+
toNullObj({ 'foo=bar': 3 }),
494+
)
491495
})
492496
})
493497

498+
function toNullObj<T>(obj: T): T {
499+
if (typeof obj === 'object') return Object.assign(Object.create(null), obj)
500+
return obj
501+
}
502+
494503
describe('relative navigation', () => {
495504
it('should navigate to a child route', async () => {
496505
const { router } = createTestRouter(

0 commit comments

Comments
 (0)