@tanstack/router-core 1.171.12 → 1.171.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.ts CHANGED
@@ -523,15 +523,26 @@ export function findLast<T>(
523
523
  }
524
524
 
525
525
  /**
526
- * Remove control characters that can cause open redirect vulnerabilities.
527
- * Characters like \r (CR) and \n (LF) can trick URL parsers into interpreting
528
- * paths like "/\r/evil.com" as "http://evil.com".
526
+ * Re-encode characters that are unsafe in URL paths.
527
+ * Includes ASCII control characters (0x00-0x1F, 0x7F) and a subset of the
528
+ * WHATWG URL "path percent-encode set" (", <, >, `, {, }).
529
+ *
530
+ * Space (0x20) is intentionally excluded — decodeURI decodes %20 to space
531
+ * and the router stores decoded spaces in location.pathname. The existing
532
+ * encodePathLikeUrl already handles re-encoding spaces for outgoing URLs.
533
+ *
534
+ * These characters are decoded by decodeURI but must remain percent-encoded
535
+ * in paths to match how upstream layers (CDNs, edge middleware, browsers)
536
+ * interpret the URL, preventing infinite redirect loops and path mismatches.
529
537
  */
538
+ // eslint-disable-next-line no-control-regex
539
+ const PATH_UNSAFE_RE = /[\x00-\x1f\x7f"<>`{}]/g
540
+
530
541
  function sanitizePathSegment(segment: string): string {
531
- // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)
532
- // These include CR (\r = 0x0D), LF (\n = 0x0A), and other potentially dangerous characters
533
- // eslint-disable-next-line no-control-regex
534
- return segment.replace(/[\x00-\x1f\x7f]/g, '')
542
+ return segment.replace(
543
+ PATH_UNSAFE_RE,
544
+ (ch) => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'),
545
+ )
535
546
  }
536
547
 
537
548
  function decodeSegment(segment: string): string {
@@ -644,8 +655,9 @@ export function decodePath(path: string) {
644
655
  result = result + decodeSegment(cursor ? path.slice(cursor) : path)
645
656
 
646
657
  // Prevent open redirect via protocol-relative URLs (e.g. "//evil.com")
647
- // After sanitizing control characters, paths like "/\r/evil.com" become "//evil.com"
648
- // Collapse leading double slashes to a single slash
658
+ // This is defense-in-depth: since control characters are no longer decoded,
659
+ // paths like "/%0d/evil.com" can no longer become "//evil.com". But we keep
660
+ // this check to guard against other edge cases.
649
661
  let handledProtocolRelativeURL = false
650
662
  if (result.startsWith('//')) {
651
663
  handledProtocolRelativeURL = true