rewrite: fix strip_path_suffix ignoring percent-encoding#7877
Open
TowyTowy wants to merge 1 commit into
Open
Conversation
StripPathSuffix is documented to behave like StripPathPrefix: the suffix
is matched in normalized (unescaped) space except where the pattern uses
an escape sequence. But suffix stripping was implemented as
reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix)))
Reversing the strings moves the '%' to the *end* of each "%xx" escape,
which defeats trimPathPrefix's escape detection (it expects '%' to
precede the two hex digits). As a result the escape-aware, normalized
comparison never happened for suffixes: a decoded pattern failed to
match a percent-encoded path.
Concretely, StripPathPrefix "/a/b/c" strips "/a%2Fb/c/d" to "/d", but the
mirror StripPathSuffix "/b/c" left "/a/b%2Fc" untouched instead of
producing "/a"; likewise StripPathSuffix "bc" did not strip "/a%62c".
This has been the behavior since caddyserver#4948, which introduced both the
escape-aware trimPathPrefix and the reverse-based suffix trimming.
Replace the reverse trick with a dedicated trimPathSuffix that iterates
from the ends of both strings and applies the same escape-aware,
case-insensitive comparison as trimPathPrefix. An escape in the pattern
itself is still compared literally, so "%2fsuffix" continues to require
the path to contain that exact escape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
steadytao
approved these changes
Jul 12, 2026
steadytao
left a comment
Member
There was a problem hiding this comment.
LGTM. The dedicated suffix scan correctly mirrors the prefix semantics while preserving literal escaped-pattern matching and the regression coverage looks sufficient.
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.
What
StripPathSuffix(uri strip_suffix/handle_pathin the Caddyfile) is documented to behave likeStripPathPrefix: the suffix is matched in normalized (unescaped) space, except where the pattern itself uses a%xxescape. That normalized comparison never actually happened for suffixes.Suffix stripping was implemented as
reverse(trimPathPrefix(reverse(path), reverse(suffix))). Reversing the strings moves the%to the end of each%xxescape, which defeatstrimPathPrefix's escape detection (it expects%to precede the two hex digits). So a decoded pattern silently failed to match a percent-encoded path.Demonstration
strip_prefix /a/b/c/a%2Fb/c/d/d/d✓/d✓strip_suffix /b/c/a/b%2Fc/a/a/b%2Fc✗/a✓strip_suffix bc/a%62c/a/a%62c✗/a✓Prefix and suffix share identical doc comments but produced different results.
Fix
Replace the reverse trick with a dedicated
trimPathSuffixthat iterates from the ends of both strings and applies the same escape-aware, case-insensitive comparison astrimPathPrefix. An escape in the pattern is still compared literally, so%2fsuffixcontinues to require the path to contain that exact escape (covered by an added negative test).Present since #4948, which introduced both the escape-aware
trimPathPrefixand the reverse-based suffix trimming.Testing
Added three cases to
TestRewrite(two fail before the fix, the negative guard passes both ways);go test ./modules/caddyhttp/...passes, gofmt/vet clean.Disclosure: this bug was found by a code audit of the rewrite handlers, and the fix and tests were prepared with the assistance of an LLM (Claude). The diagnosis was verified by a human via fail-before/pass-after runs.