Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions packages/router-core/src/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,43 @@ export const defaultStringifySearch = stringifySearchWith(
* The returned function strips a leading `?`, decodes values, and attempts to
* JSON-parse string values using the given `parser`.
*
* To prevent lossy coercion of opaque strings (hex codes, large integers,
* scientific-notation numbers, etc.) the parsed value is only accepted when
* re-serializing it with `stringify` reproduces the original string.
* This rejects conversions like `'662E41'` → `6.62e+43` and
* `'723421968459640832'` → `723421968459640800`.
*
* @param parser Function to parse a string value (e.g. `JSON.parse`).
* @param stringify Function to re-serialize the parsed value for the
* roundtrip check. Defaults to `JSON.stringify`. Pass a matching
* serializer when `parser` is something other than `JSON.parse`.
* @returns A `parseSearch` function compatible with `Router` options.
* @link https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization
*/
export function parseSearchWith(parser: (str: string) => any) {
export function parseSearchWith(
parser: (str: string) => any,
stringify: (val: any) => string = JSON.stringify,
) {
return (searchStr: string): AnySchema => {
if (searchStr[0] === '?') {
searchStr = searchStr.substring(1)
}

const query: Record<string, unknown> = decode(searchStr)

// Try to parse any query params that might be json
// Try to parse any query params that might be json. A roundtrip check
// guards against lossy coercion: strings like `"662E41"` or
// `"723421968459640832"` parse to valid JSON values, but the original
// string cannot be recovered once the parsed number/string has been
// re-serialized.
for (const key in query) {
const value = query[key]
if (typeof value === 'string') {
try {
query[key] = parser(value)
const parsed = parser(value)
if (stringify(parsed) === value) {
query[key] = parsed
}
} catch (_err) {
// silent
}
Expand Down
18 changes: 18 additions & 0 deletions packages/router-core/tests/searchParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ describe('Search Params serialization and deserialization', () => {
expect(defaultStringifySearch(obj)).not.toBe(input)
})

/*
* Regression coverage for #7650: opaque strings that happen to be valid
* JSON (hex codes, large integers, scientific-notation numbers) must
* not be destructively coerced into JSON values. The default parser
* uses a roundtrip check that rejects parses whose re-serialization
* does not match the original string.
*/
test.each([
['?codAut=662E41', { codAut: '662E41' }],
['?id=723421968459640832', { id: '723421968459640832' }],
['?sig=abcdef0123456789', { sig: 'abcdef0123456789' }],
['?ulid=01H8XGJWBWBAQ4ZEXAMPLE0000', {
ulid: '01H8XGJWBWBAQ4ZEXAMPLE0000',
}],
])('opaque string roundtrip %s', (input, expected) => {
expect(defaultParseSearch(input)).toEqual(expected)
})

/*
* It can serialize stuff that really shouldn't be passed as input.
* But just in case, this test serves as documentation of "what would happen"
Expand Down