Stop double-encoding %2F in match.pathname - #15398
Open
MahinAnowar wants to merge 2 commits into
Open
Conversation
`useRoutesImpl` re-encodes the pathname that `matchRoutes` decoded, and did so by replacing every `%` with `%25`. But `matchRoutes` decodes a segment at a time via `decodePath`, which escapes any `/` that decoding produced back to `%2F` so it does not become a segment boundary. Those `%` characters were then re-encoded too, so a URL like `/abc%2Fxyz/child` resolved to `/abc%252Fxyz/child` in `<Form>` actions and `<Link>` hrefs. The decoded pathname cannot be re-encoded on its own: a `%2F` in it may have come from `%2F` (an escaped slash) or from `%252F` (a literal "%2F"), and both decode to the same string. Dropping the `%` replacement fixes the first and breaks `%25`; keeping it does the reverse. Decoding preserves the number of segments, so map the decoded match pathname back onto the encoded pathname by segment index instead. That is the same trick used just above to strip the parent base, and it keeps `%25` and `%252F` intact because their encoded segments are taken verbatim. Fixes remix-run#14619
Contributor
✅ CLA SignedThanks for signing the Contributor License Agreement. |
Contributor
✅ Change File FoundOne or more change files found.
|
5 tasks
Resolves a conflict in useRoutesImpl: main restructured the matches computation into an if/else that adds a dataRouterOpts.router.match(state.location) branch. Kept that structure verbatim and left the toEncodedPathname helper ahead of it, so the re-encoding fix applies to every branch as before.
Contributor
Author
|
Merged main in — this had gone conflicted, which meant CI wasn't running on it at all. The conflict was in
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
useRoutesImplre-encodes the pathname thatmatchRoutesdecoded, and it did that by replacing every%with%25. ButmatchRoutesdecodes a segment at a time throughdecodePath, which escapes any/that decoding produced back to%2Fso it doesn't turn into a segment boundary:Those
%characters get re-encoded along with everything else, so a URL like/abc%2Fxyz/childcomes out as/abc%252Fxyz/childin<Form>action attributes and<Link>hrefs.The decoded pathname can't be re-encoded on its own. A
%2Fin it may have come from%2Fin the URL (an escaped slash) or from%252F(a literal"%2F"), and both decode to the same string. I checked both of the fixes suggested on the issue against the four shapes and each one trades one bug for another:main%replacement%replacement, skipping%2F/abc%2Fxyz/child%252F❌/a%25b/child/a%b❌/a%252Fb/child%2F❌/2%25%200/childDropping the
%replacement regresses #14510, which is what added it in #14786.Decoding preserves the number of segments though —
decodePathsplits on/and joins on/— so the decoded match pathname can be mapped back onto the encoded pathname by segment index. That avoids re-encoding entirely, and it's the same approach used about twenty lines above to strip the parent base, for the same reason:%25and%252Fkeep working because their encoded segments are now taken verbatim rather than reconstructed.Related Issue
Fixes #14619
There's an earlier PR for this issue, #14621, which has been conflicting since June and takes a different approach (a
safeEncodeingeneratePaththat skips already-encoded sequences). I went at it from thematch.pathnameside that @MicheleBertoli identified in the issue thread — the bisect to #14786 in that comment is what pointed me at the right line. Happy to close this if that one gets picked back up.Worth noting the originally reported symptom (params coming out as
2%%200%20g%20-%202) no longer reproduces onmain— params decode correctly now. What's left is thematch.pathnameshape from the later comment.Motivation and Context
%2Fin a path segment is how a value containing a slash gets through a URL, which is common for base64 tokens and encoded IDs. Right now any route matching such a segment renders a<Form>whose action points at a different URL than the one the user is on, so the POST 404s or hits the wrong route.How Has This Been Tested?
Added
handles encoded slashes in ancestor splat route segmentstospecial-characters-test.tsx, next to the existinghandles encoded percent signstest it mirrors. It asserts the<Link>href stays/parent/child/slash-%2F-sign/grandchildand the param decodes toslash-/-sign. Reverting justhooks.tsxand keeping the test fails with%252F.I also ran the four-shape table above as a temporary SSR test using
createStaticHandler+renderToStaticMarkup(the repro from the issue) before and after, which is where the comparison numbers come from.Full package suite: 2208 passing, 824 snapshots, up one test from the 2207 on
main. Two suites (rsc/server-test.ts,dom/dom-export-test.tsx) fail identically before and after on a clean checkout here — they resolve build output that isn't present without a build.tsc --noEmit, eslint andscripts/changes/validate.tsare clean.Types of changes
Checklist:
contributors.yml).