79 Commits
Author SHA1 Message Date
Matthew HoltandCopilot 7593fd002f rewrite: clean paths before regexp replacement changes depth
Canonicalize the path before capturing and substituting its components, not only afterward. Retain repeated slashes, query strings and valid alternate encodings; cover the supplied depth-changing traversal and encoded dot segments.

Addresses scan report 34013c71-0786-4179-8403-eacc143a1d89 (marked duplicate, but not fixed by the prior post-replacement safeguard).

Code written by GitHub Copilot (GPT-6.1 Sol, Claude Opus 5.5); reviewed by mholt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-11 11:06:41 -07:00
Matthew HoltandCopilot 3c65959560 rewrite: escape decoded original and component path placeholders
Decoded path placeholders (orig_uri.path, path components and file
matcher values) were inserted raw into the rewritten URI, so an
encoded '%' or '?' could be decoded a second time. Escape all decoded
path placeholders in path context in a single replacement pass; escaped
and whole-URI placeholders keep their URI syntax.

Addresses scan report 9e3a082391f793d1.

Code written by GitHub Copilot (GPT-6.1 Sol, Claude Opus 5.5); reviewed by mholt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-11 11:06:40 -07:00
Matthew HoltandCopilot fc6c202ea6 rewrite: canonicalize paths after substring and regexp replacement
Like prefix/suffix stripping, substring and regexp path replacement can
produce a relative path or expose dot segments that later path-scoped
handlers interpret differently. Apply the existing canonicalizePath
safeguard whenever these operations change the path.

Addresses scan report 65568b0d767b733c.

Code written by GitHub Copilot (GPT-6.1 Sol, Claude Opus 5.5); reviewed by mholt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-11 11:06:40 -07:00
Julien Voisin c8492563ee caddyhttp: presize buffers in hot paths to reduce allocations (#7842)
* caddyhttp: presize buffers in hot paths to reduce allocations

Right-size three per-request allocations that previously grew from a
nil/empty backing array, causing repeated reallocation:

- CleanPath: pre-Grow the strings.Builder used when slash collapsing is
  disabled, instead of letting it reallocate as runes are appended
- headers: presize the rewritten value slice in HeaderOps.ApplyTo's Set
  loop instead of appending from nil
- reverseproxy: presize the weights slice in WeightedRoundRobinSelection
  .Select

This commit also adds benchmarks exercising each path. Measured with benchstat
with count=20 and GOMAXPROCS=1, with p=0.000:

                                    sec/op                  B/op              allocs/op
CleanPathNoCollapse       1013.4n -> 828.6n  (-18.23%)  120->64  (-46.67%)   4->1  (-75.00%)
HeaderOpsApplyToSet       1262.0n -> 955.8n  (-24.27%)  255->152 (-40.39%)   9->6  (-33.33%)
WeightedRoundRobinSelect   215.8n -> 200.3n  ( -7.20%)  112->96  (-14.29%)   2->2  (N/A)

* caddyhttp: presize strings.Builder in query rewrite and path matcher

Two builders whose output size is bounded by a known input length were
growing from empty, causing repeated reallocation. Seed each with a
capacity hint:

- rewrite.buildQueryString: pre-Grow to len(qs) (runs per request when
  rewrite query manipulation is configured)
- MatchPath.matchPatternWithEscapeSequence: pre-Grow to len(escapedPath)
  (reached only when a path pattern contains % escape sequences)

Adds a benchmark for buildQueryString, measured with benchstat:

BuildQueryString:
	- sec/op  965.2n -> 743.3n  (-22.99%)
	- B/op 248->80  (-67.74%)
	- allocs/op 5->1  (-80.00%)

The builder previously reallocated four times per call. The matcher path
is cold and not separately benchmarked.

* chore: add benchmark headers

---------

Signed-off-by: Julien Voisin <jvoisin@users.noreply.github.com>
2026-11-04 20:26:53 +00:00
Zen Dodd 43653cfb65 rewrite: treat only valid percent encodings as escapes (#8345) 2026-11-03 03:11:06 -05:00
Nguyen Thanh Dat c645c2b439 rewrite: fix strip_path_suffix decoding a literal '%' as an escape (#8005) 2026-11-03 01:31:54 -05:00
Kévin Dunglas 094172da84 rewrite: escape file matcher remainder before rewriting (#8325) 2026-11-01 15:53:43 -05:00
Zen Dodd ef1877210e Merge commit from fork
* caddyhttp: canonicalise paths after prefix stripping

* caddyhttp: cover suffix stripping in path canonicalisation
2026-09-14 12:50:44 -06:00
TowyTowyandClaude Fable 5 7bf1b9057b rewrite: fix strip_path_suffix ignoring percent-encoding (#7877)
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 #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>
2026-08-28 19:44:05 +10:00
Francis LavoieandClaude Opus 5 39af0aec31 rewrite: fix URI splitting when a query or fragment arrives via a placeholder (#7947)
* rewrite: don't drop a trailing '=' from the query string

buildQueryString scanned for '=' unconditionally when looking for the
end of a component, but '=' only delimits a key from its value once;
any further '=' bytes are literal data. When the query ended with '=',
that byte was consumed as a delimiter with nothing following it to
re-emit, so it was silently lost:

	?x=1&sig=YWJjZA==  =>  ?x=1&sig=YWJjZA=

This corrupts base64 padding in the last query parameter, which is the
shape of an S3 presigned URL (X-Amz-Signature), turning a valid
signature into a 403. Only the final '=' of the query was affected;
?sig=YWJjZA==&x=1 came through intact.

Disable the '=' search while consuming a value so that only '&' ends it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite: honor a query string injected by a replacement value

Which URI components get written back was decided from the literal
config string, before placeholders were expanded, but an injected query
is only detected after expansion. The two were never reconciled: for
`rewrite * {rp.header.X-Accel-Redirect}` there is no literal '?', so
qsStart stayed -1, and the correctly-built query string was computed and
then discarded by the `if qsStart >= 0` guard.

Only half the split was applied. The path was still truncated at the
injected '?', so the query was not preserved either -- it was dropped,
and any query already on the request survived in its place:

	GET /orig?keep=me, X-Accel-Redirect: /hello?some=param
	=> /hello?keep=me

Track whether a query was actually injected and include that in the
write-back condition. Appending a literal '?' to the rewrite value was
the known workaround precisely because it set qsStart; that keeps
working and is now unnecessary.

The flag is only set where the injected query is adopted, so an
explicitly configured query still wins, and a value with no '?' still
leaves the query untouched -- which is what the implicit rewrites of
try_files and php_fastcgi rely on. Those stay safe regardless, since
escapePathPlaceholders already escapes the two placeholders they use, so
a client-supplied %3F cannot split the URI.

Fixes #5208

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite: drop a fragment injected by a replacement value

The scan that separates path, query and fragment runs on the literal
config string, so a '#' arriving later via a replacement value was never
treated as a delimiter. It leaked into whichever component it landed in:

	X-Accel-Redirect: /hello?p=x#frag  =>  RawQuery = "p=x#frag"

Everything after '#' is fragment (RFC 3986 section 4.2) and a fragment is
never sent to the server, so drop it before the path is split, mirroring
how the scan already handles a literal '#'. An escaped %23 is unaffected,
so a real '#' in a path or query is still expressible, and a configured
fragment still wins over an injected one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:06:09 -06:00
Saleh c96bca1269 rewrite: preserve non-canonical path encoding after uri replace (#7907)
changePath cleared RawPath whenever it equalled url.Path, which
discarded a valid non-canonical percent-encoding produced by a
replacement. This compare `RawPath` against the default escaping of Path
instead.
2026-07-26 08:49:44 +10:00
alhuda ae9bc028e6 rewrite: scope keyed query replace to its named key (#7818)
* rewrite: scope keyed query replace to its named key

* rewrite: cover keyed search_regexp query replace in test

* rewrite: provision query replace test via Provision path
2026-06-16 01:01:05 +10:00
alhuda 52dc6709fb rewrite: fix wrong index check in trimPathPrefix (#7812) 2026-06-12 14:11:21 -06:00
LohitandMatt Holt 176b043b01 rewrite: prevent placeholder re-expansion in injected query (#7761)
When the rewrite URI template ends with a literal '?' and contains a placeholder that expands to client-controlled bytes (e.g. {http.request.header.X-Fwd}), those bytes flow into buildQueryString which runs a second Replacer pass. If the bytes contain placeholder syntax such as {env.SECRET}, that placeholder is evaluated, allowing disclosure of environment variables, files (via {file./path}), or internal request vars through the rewritten request URI.

Escape '{' and '}' in the injected query before assigning it to the query variable, so the second pass cannot find any placeholder syntax to evaluate. Operator-written placeholders in the rewrite template are already expanded by the first pass on the path component, so the only '{' or '}' surviving into the injected query must have come from replacement values.

Fixes GHSA-j8px-rmrx-76h9.

Includes three regression tests mirroring the 'is not re-expanded' tests in modules/caddyhttp/vars_test.go.

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-05-26 16:51:18 -06:00
Rayan Salhabandcyphercodes a4a38c3e88 rewrite: escape file matcher paths before rewriting (#7683)
* fix: escape file matcher paths in rewrites

Preserve matched file paths containing literal '?' or '%' when try_files rewrites to http.matchers.file.relative.

* test: cover nested escaped try_files rewrite paths

* test: cover encoded slash try_files rewrite paths

* fix: assert file matcher placeholder as string

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-05-11 17:16:33 -06:00
Tao 6f6771aa1d rewrite: skip query rename when source key is absent (#7599) 2026-03-28 13:10:34 -04:00
Matthew Holt fbfb8fc517 rewrite: Force recomputing path when escaped path matches rewrite target
Thank you for the report by @MaherAzzouzi, and the suggested fix!
2026-03-04 16:18:33 -07:00
Francis Lavoie 90972fbebc chore: Dumb prealloc lint fix (#7430) 2026-01-13 14:13:43 -05:00
Mohammed Al Sahaf e039a5bb5c chore: upgrade .golangci.yml and workflow to v2 (#6924)
* chore: upgrade .golangci.yml and workflow to v2

run `golangci-lint fmt`

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* run `golangci-lint run --fix`

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* more lint fixes

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* bring back comments to .golangci.yml

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* appease the linter some more

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* oops

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* use embedded structs

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* use embedded structs where they were used before

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* disable rule  `-QF1006`

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* missed a spot

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2025-06-03 02:24:32 +03:00
Francis Lavoie 5823eccf99 rewrite: Don't add / in Caddyfile, do it after replacer (#6662) 2024-11-05 10:15:31 -07:00
Kévin Dunglas f4bf4e0097 perf: use zap's Check() to prevent useless allocs (#6560)
* perf: use zap's Check() to prevent useless allocs

* fix

* fix

* fix

* fix

* restore previous replacer behavior

* fix linter
2024-09-13 11:16:37 -06:00
mister-turtle 21f9c20a04 rewrite: Avoid panic on bad arg count for uri (#6571) 2024-09-13 03:22:03 -04:00
Alexander Stecher ffd28be90a rewrite: Only serialize request if necessary (#6541)
* Prevents serializing the caddy request if log level is not debug.

* Extracts message to const.
2024-08-30 08:46:51 -06:00
Aziz Rmadi db9d167354 Added a null check to avoid segfault on rewrite query ops (#6191) 2024-03-23 01:51:34 -04:00
Aziz Rmadi 29f57faa86 rewrite: uri query replace operation (#6165)
* Implemented query replace oeration

* Modified replace operation to use regexes in caddyfile

* Added more tests to uri query operations
2024-03-22 02:23:42 +00:00
Aziz Rmadi 69290d232d rewrite: Implement uri query operations (#6120)
* Implemented basic uri query operations

* Added support for query operations block

* Applied Replacer on all query keys and values

* Implemented rename query key opration

* Rewrite struct: Changed QueryOperations field to Query and comments cleanup

* Cleaned up comments, changed the order of operations and added more tests

* Changed order of fields in queryOps struct to match the operations order
2024-03-06 10:08:46 -05:00
Francis Lavoie 750d0b8331 caddyfile: Normalize & flatten all unmarshalers (#6037) 2024-01-23 19:36:59 -05:00
Francis Lavoie 5e2f1b5ced httpcaddyfile: Rewrite root and rewrite parsing to allow omitting matcher (#5844) 2024-01-15 09:57:08 -07:00
a c839a98ff5 filesystem: Globally declared filesystems, fs directive (#5833) 2024-01-13 20:12:43 +00:00
Jacob Gadikian d6f86cccf5 ci: use gci linter (#5708)
* use gofmput to format code

* use gci to format imports

* reconfigure gci

* linter autofixes

* rearrange imports a little

* export GOOS=windows golangci-lint run ./... --fix
2023-08-14 09:41:15 -06:00
Tran Phong 13a37688dc rewrite: use escaped path, fix #5278 (#5504)
* use escaped path while rewriting

Signed-off-by: TP-O <letranphong2k1@gmail.com>

* restore line break

---------

Signed-off-by: TP-O <letranphong2k1@gmail.com>
2023-05-16 09:16:07 -06:00
Francis Lavoie ee7c92ec9b reverseproxy: Mask the WS close message when we're the client (#5199)
* reverseproxy: Mask the WS close message when we're the client

* weakrand

* Bump golangci-lint version so path ignores work on Windows

* gofmt

* ugh, gofmt everything, I guess
2022-11-14 09:38:02 -07:00
Matthew Holt 013b510352 rewrite: Only trim prefix if matched
See #5073
2022-09-28 00:13:12 -06:00
Matthew Holt e43b6d8178 core: Variadic Context.Logger(); soft deprecation
Ideally I'd just remove the parameter to caddy.Context.Logger(), but
this would break most Caddy plugins.

Instead, I'm making it variadic and marking it as partially deprecated.
In the future, I might completely remove the parameter once most
plugins have updated.
2022-09-16 16:55:36 -06:00
Matt HoltandRussellLuo a479943acd caddyhttp: Smarter path matching and rewriting (#4948)
Co-authored-by: RussellLuo <luopeng.he@gmail.com>
2022-08-16 08:48:57 -06:00
Chirag Maheshwari d26559316f Replace strings.Index with strings.Cut (#4932) 2022-08-06 22:03:37 -06:00
Francis Lavoie 141872ed80 chore: Bump up to Go 1.19, minimum 1.18 (#4925) 2022-08-02 16:39:09 -04:00
Matthew Holt 693e9b5283 rewrite: Handle fragment before query (fix #4775) 2022-05-09 11:09:42 -06:00
Francis Lavoie f6900fcf53 reverseproxy: Support performing pre-check requests (#4739) 2022-05-06 10:50:26 -04:00
Francis Lavoie bcb7a19cd3 rewrite: Add method Caddyfile directive (#4528) 2022-01-18 12:17:35 -07:00
Francis Lavoie 1feb65952a rewrite: Fix a double-encode issue when using the {uri} placeholder (#4516) 2022-01-13 12:17:15 -05:00
Matthew Holt ad8d01cb66 rewrite: Implement regex path replacements
https://caddy.community/t/collapsing-multiple-forward-slashes-in-path-only/11626
2021-03-01 18:27:59 -07:00
go-d 88a38bd00d rewrite: Use RawPath instead of Path (fix #3596) (#3918)
Prevent information loss, i.e. the encoded form that was sent by the
client, when using URL strip/replace.
2021-01-11 09:18:53 -07:00
Francis Lavoie 8c5d00b2bc httpcaddyfile: New handle_path directive (#3281)
* caddyconfig: WIP implementation of handle_path

* caddyconfig: Complete the implementation - h.NewRoute was key

* caddyconfig: Add handle_path integration test

* caddyhttp: Use the path matcher as-is, strip the trailing *, update test
2020-05-26 15:27:51 -06:00
Matthew Holt 809e72792c rewrite: Fix for rewrites with URI placeholders (#3209)
If a placeholder in the path component injects a query string such as
the {http.request.uri} placeholder is wont to do, we need to separate it
out from the path.
2020-04-01 00:43:40 -06:00
Matthew Holt 105acfa086 Keep type information with placeholders until replacements happen 2020-03-30 11:49:53 -06:00
Matt Holt aa6c5fde07 httpcaddyfile: Unify strip_prefix, strip_suffix, uri_replace directives (#3157)
* rewrite: strip_prefix, strip_suffix, uri_replace -> uri (closes #3140)

* Add period, to satisfy @whitestrake :) and my own OCD

* Restore implied / prefix
2020-03-19 11:51:28 -06:00
Matthew Holt 0742530d3d rewrite: Prepend "/" if missing from strip path prefix
Paths always begin with a slash, and omitting the leading slash could be
convenient to avoid confusion with a path matcher in the Caddyfile. I do
not think there would be any harm to implicitly add the leading slash.
2020-01-22 09:36:05 -07:00
Matthew Holt e51e56a494 httpcaddyfile: Fix nested blocks; add handle directive; refactor
The fix that was initially put forth in #2971 was good, but only for
up to one layer of nesting. The real problem was that we forgot to
increment nesting when already inside a block if we saw another open
curly brace that opens another block (dispenser.go L157-158).

The new 'handle' directive allows HTTP Caddyfiles to be designed more
like nginx location blocks if the user prefers. Inside a handle block,
directives are still ordered just like they are outside of them, but
handler blocks at a given level of nesting are mutually exclusive.

This work benefitted from some refactoring and cleanup.
2020-01-16 17:08:52 -07:00
Matthew Holt 07ad4655db rewrite: Make URI modifications more transactional (#2891)
Before, modifying the path might have affected how a new query string
was built if the query string relied on the path. Now, we build each
component in isolation and only change the URI on the request later.

Also, prevent trailing & in query string.
2020-01-15 11:44:21 -07:00