TanStack / TanStack/router

defaultParseSearch corrupts string search params that look like JSON numbers (e.g. "662E41", large integers) — lossy and unrecoverable on inbound URLs

Open
#7,650 0 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

Which project does this relate to?

Router

Describe the bug

defaultParseSearch runs JSON.parse on every search-param value and keeps the
result whenever it parses to a number. This destructively coerces values that are
opaque strings
, not numbers — authorization codes, signatures/HMACs, hex IDs,
all-digit ULIDs, etc. — as long as they match the JSON number grammar.

Two consequences:

  1. Lossy / irreversible. 662E41 (a valid 6-char hex/auth code) → 6.62e+43;
    723421968459640832723421968459640800. The original string cannot be
    reconstructed.
  2. Unrecoverable for inbound/external URLs. When the value arrives from an
    external redirect (OAuth callback, payment-gateway return, …), the coercion
    happens inside parseSearch before validateSearch runs. By the time a Zod
    schema sees it, it is already a lossy number — z.coerce.string() / z.string()
    cannot recover the original, they only see 6.62e+43.

This is the data-corruption root cause underneath the quote-wrapping symptom in
#6044 and the string/number typing confusion in #537 / discussion #430, but the
failure mode here is destruction of non-numeric data, not formatting.

Complete minimal reproducer

https://stackblitz.com/edit/github-hjjlagts?file=src%2Froutes%2Findex.tsx

Steps to Reproduce the Bug
import { defaultParseSearch } from '@tanstack/router-core' // re-exported by @tanstack/react-router

defaultParseSearch('?codAut=662E41')
// actual:   { codAut: 6.62e+43 }
// expected: { codAut: "662E41" }

defaultParseSearch('?id=723421968459640832')
// actual:   { id: 723421968459640800 }   // precision loss
// expected: { id: "723421968459640832" }

A route with validateSearch: z.object({ codAut: z.string() }) cannot guard an
inbound value: it's already a number before Zod runs.

Expected behavior

A param value that does not survive a numeric round-trip should be kept as the
original string. Canonical numbers / booleans / objects should still be coerced.

Screenshots or Videos

No response

Platform
  • Router / Start Version: @tanstack/react-router 1.168.25 (router-core 1.168.17) · @tanstack/react-start 1.167.50
  • OS: macOS 26.5 (Darwin 25.5.0)
  • Browser: Chrome
  • Browser Version: 149.0.0
  • Bundler: vite
  • Bundler Version: 7.3.2
Additional context

Root cause
qss's toValue already guards against lossy coercion:

// qss.ts — toValue
return +str * 0 === 0 && +str + "" === str ? +str : str; // keep string if it doesn't round-trip

…but parseSearchWith(JSON.parse) re-applies JSON.parse to the strings toValue
deliberately left alone, defeating that guard:

// searchParams.ts — parseSearchWith
const query = decode(searchStr);          // toValue kept "662E41" as a string
for (const key in query) {
  if (typeof query[key] === 'string')
    query[key] = JSON.parse(query[key]);   // => 6.62e+43 (re-introduces precision loss)
}

Proposed fix
Apply the same round-trip guard in the default parser — accept JSON.parse's
result only when it isn't a number, or when String(result) === input; otherwise
keep the original string:

const parsed = JSON.parse(value)
return typeof parsed === 'number' && String(parsed) !== value ? value : parsed

Preserves coercion for "2", "true", "0.5", {"a":1}, …, while keeping
662E41, large integers, leading-zero strings and other non-round-tripping values
intact. Happy to open a PR if the team agrees with the direction.

Real-world impact
A payment-gateway return URL echoes an auth code (codAut) the backend uses to
recompute an HMAC. Coercing 662E41 → "6.62e+43" corrupts the field, the
signature no longer matches, and the capture is rejected (HTTP 403) even though the
payment succeeded.

Related
#6044 — quote-wrapping (downstream symptom of the same coercion)
#537 — string/number coercion typing
Discussion #430

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in searchParams.ts at parseSearchWith and compare its JSON.parse handling with the round-trip guard in qss.ts. Reproduce the defaultParseSearch examples for 662E41 and 723421968459640832, then add regression coverage showing canonical values still coerce while lossy numeric-looking strings remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
web-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.