Skip to content

rewrite: fix strip_path_suffix ignoring percent-encoding#7877

Open
TowyTowy wants to merge 1 commit into
caddyserver:masterfrom
TowyTowy:fix/strip-path-suffix-percent-encoding
Open

rewrite: fix strip_path_suffix ignoring percent-encoding#7877
TowyTowy wants to merge 1 commit into
caddyserver:masterfrom
TowyTowy:fix/strip-path-suffix-percent-encoding

Conversation

@TowyTowy

Copy link
Copy Markdown
Contributor

What

StripPathSuffix (uri strip_suffix / handle_path in the Caddyfile) is documented to behave like StripPathPrefix: the suffix is matched in normalized (unescaped) space, except where the pattern itself uses a %xx escape. 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 %xx escape, which defeats trimPathPrefix's escape detection (it expects % to precede the two hex digits). So a decoded pattern silently failed to match a percent-encoded path.

Demonstration

operation path expected before after
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 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 is still compared literally, so %2fsuffix continues to require the path to contain that exact escape (covered by an added negative test).

Present since #4948, which introduced both the escape-aware trimPathPrefix and 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.

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 steadytao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The dedicated suffix scan correctly mirrors the prefix semantics while preserving literal escaped-pattern matching and the regression coverage looks sufficient.

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.

2 participants