Skip to content

normalize: decode %2E in a path segment that isn't a dot-segment - #212

Open
dngr2 wants to merge 1 commit into
fastify:mainfrom
dngr2:decode-path-dot
Open

normalize: decode %2E in a path segment that isn't a dot-segment#212
dngr2 wants to merge 1 commit into
fastify:mainfrom
dngr2:decode-path-dot

Conversation

@dngr2

@dngr2 dngr2 commented Aug 17, 2026

Copy link
Copy Markdown

normalize() leaves %2E percent-encoded everywhere in the path, so http://h/foo%2Ebar stays foo%2Ebar and equal('.../foo%2Ebar', '.../foo.bar') returns false. But . is unreserved (RFC 3986 §2.3) and should be decoded (§6.2.2.2) — the query and fragment normalizers already do, and every other unreserved byte (- _ ~) is decoded in the path.

The blanket decoded !== '.' guard exists to stop %2E%2E from decoding into .. and being collapsed by removeDotSegments (a traversal defense), but it is too broad. This decides per segment: %2E stays encoded only when the segment is exactly . or .., and decodes otherwise. /public/%2E%2E/admin is unchanged and the security-normalization tests still pass.

normalizePathEncoding kept every %2E percent-encoded to stop %2E%2E from
decoding into .. and being collapsed by removeDotSegments. That suppressed %2E
everywhere, so foo%2Ebar and %2Ehidden stayed encoded even though . is
unreserved (RFC 3986 2.3) and should be decoded (6.2.2.2) — the query and
fragment normalizers already do, and equal() reported RFC-equivalent URIs as
unequal. Decide per segment: keep %2E encoded only when the segment is . or ..,
decode it otherwise, so the %2E%2E traversal stays encoded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant