491 Commits
Author SHA1 Message Date
Islam Elsayed 56ae39bdcc reverseproxy: replace only known placeholders in health check body (#8003)
The active health check body is user-supplied and commonly JSON, but it
was expanded with ReplaceAll, which blanks any `{...}` the replacer does
not recognize. Only globals are available here, so a JSON
health_request_body was sent as an empty body.

This is not limited to bodies without placeholders. A body that does use
one is mangled too: `{"token":"{env.SOME_VAR}"}` is sent as `"}`, since
the opening `{"token":"` is consumed as an unrecognized placeholder
before the real one is reached.

Use ReplaceKnown, matching the header values a few lines below, so
unrecognized braces are left intact and real placeholders still expand.

Only the body is changed. The Host header on the same path keeps
ReplaceAll deliberately: Go's HTTP server blanks a Host containing
braces, so leaving `{...}` intact there would send a Host the upstream
discards, where blanking it lets Go fall back to the URL host.
2026-09-09 08:48:08 -06:00
bzyy1024 45ba3278b5 fastcgi: fix HTTPoxy vulnerability (#7934)
* Implement HTTPoxy mitigation in FastCGI
Added HTTPoxy mitigation to prevent trusting client-supplied Proxy header for HTTP_PROXY environment variable.

* Implement test for HTTPoxy vulnerability protection
Add test to ensure HTTPoxy vulnerability is mitigated by dropping client-supplied Proxy headers.

* gofmt: format code
* revert unrelated gofmt change to replacer_test.go
2026-08-19 13:07:24 +10:00
yangshenghui 0bbda5c728 fix: close resources on error paths (#7940) 2026-08-16 21:02:39 +10:00
6584c75999 reverseproxy: isolate active health-check state per distinct check config (#7916)
* reverseproxy: isolate active health-check state per distinct check config

Multiple reverse_proxy handlers configured with different active health
checks (health_uri, health_headers, ...) against the same upstream dial
address currently share a single Host in the global pool, so one
handler's failing probes mark the address unhealthy for every other
handler. Key the pool by dial address plus a stable fingerprint of the
active health-check config, so distinct checks get independent health
state.

The fingerprint is strictly internal to pool identity: the Prometheus
upstreams_healthy label and the /reverse_proxy/upstreams admin endpoint
continue to report the plain dial address, unchanged.

Dynamic upstreams are intentionally out of scope here: they resolve
through a separate per-lookup path (dynamicHosts) and collapsing there
has different lifetime semantics; noted for a follow-up.

Fixes #7870

* reverseproxy: use strings.Cut in hostKeyAddress

Satisfies the modernize linter; behaviour is unchanged, since Cut returns
the whole string when the separator is absent.

* reverseproxy: expose the health-check fingerprint as a public discriminator

Health state is now kept per (dial address, active health check config),
but both user-visible surfaces still reported address alone:

- caddy_reverse_proxy_upstreams_healthy was labeled only by upstream, so
  every handler sharing an address wrote the same series concurrently and
  the reported value was whichever updater ran last. The metric gains a
  health_check label carrying the config fingerprint ("" when no active
  checks), so each health target owns its series; aggregate across checks
  with sum/min by (upstream).

- /reverse_proxy/upstreams reported one entry per pool key but with only
  the plain address, so consumers indexing by address silently discarded
  all but one entry. Entries now carry health_check (omitted when empty),
  and the endpoint documents that (address, health_check) is the entry's
  identity — one entry per health target, deliberately not aggregated,
  since any aggregation here would be lossy and undocumented.

Tests: two handlers on one address with different checks must produce two
metric series reflecting their own state (fails if the fingerprint is
dropped from the label), and two admin entries distinguished by non-empty
fingerprints.

* reverseproxy: narrow the fix to per-Upstream active health counters

Move the consecutive active pass/fail counters from Host onto Upstream,
alongside the active unhealthy state that already lives there, instead of
re-keying the global host pool.

Host is keyed by dial address alone, but an active health check is
configured per handler, so two handlers dialing the same address with
different health_uri or health_headers share those counters and can push
each other over their own thresholds. Upstream is already per-handler and
already carries the active unhealthy flag, so the counters belong next to
it and the pool keeps its plain dial-address keys.

This drops the host key fingerprint and its exposure in the metric label
and the admin upstreams endpoint; the metric series identity is left for
separate consideration.

---------

Co-authored-by: SillyZir <269283839+SillyZir@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-08-12 15:20:15 +10:00
Kévin Dunglas 947087cadd Merge commit from fork
Caddy's underscore header filter (GHSA-f59h-q822-g45g) only checked
for `_`. PHP folds `.` to `_` when registering $_SERVER keys the same
way CGI/FastCGI folds `-` to `_`, so a dotted alias (e.g. Remote.User)
survived the filter and collided with the legitimate hyphenated
header once it reached a PHP/FastCGI backend, bypassing forward_auth
copy_headers the same way the underscore alias did.

Extends the filter to drop `.` symmetrically, adds an
`expected_dot_headers` allowlist mirroring `expected_underscore_headers`,
and handles header names containing both separators (only an exact
allowlist entry is honored there, since a prefix glob's free-form
suffix can't be vetted for an embedded second separator).

Root cause identified by @iliaal in the FrankenPHP advisory
GHSA-49wc-4hcv-v58q.
2026-08-11 08:00:15 -06:00
Julien Voisin 22e976e5ae reverseproxy: preallocate upstream slices with known sizes (#7926)
Preallocate the sources slice in MultiUpstreams.Provision and the
netAddrs slice in UpstreamResolver.ParseAddresses now that their final
lengths are known, avoiding the intermediate append regrowth
allocations. For a 4-address resolver this reduces ParseAddresses from
11 to 9 allocs/op and 528 to 384 B/op; the benefit scales with the
number of sources/addresses.

                                 |  before  |            after            |
                                 |  sec/op  |  sec/op     vs base         |
UpstreamResolverParseAddresses-8 | 2.289µ   |1.946µ     ~ (p=0.165) noisy |

B/op:      528 → 384   -27.27%  (p=0.000)
allocs/op:  11 →   9   -18.18%  (p=0.000)

Adds BenchmarkUpstreamResolverParseAddresses as per policy.
2026-08-10 15:51:50 +10:00
Julien Voisin 0476754858 reverseproxy: reduce allocation/CPU overhead in request hot paths (#7925)
Build the Via request and response headers with strconv-based
concatenation instead of fmt.Sprintf, avoiding fmt's reflection and
formatting overhead on every proxied request/response. Same single
allocation, ~40% faster for that construction in isolation.

                 | before (fmt) |            after (strconv)          |
                 |    sec/op    |    sec/op     vs base               |
PrepareRequest-8 | 6.136µ ± 11% | 4.491µ ± 33%  -26.80% (p=0.007 n=10)|

B/op:     1008 -> 1008   (unchanged)
allocs/op:  12 -> 12     (unchanged)

No behavior change. Adds BenchmarkPrepareRequest, as per policy, exercising the
request preparation path.
2026-08-10 15:49:55 +10:00
Renich Bon Ciric 546752e214 feat(fastcgi): populate SERVER_ADDR by default (#7912)
* feat(fastcgi): populate SERVER_ADDR by default

Populate the SERVER_ADDR FastCGI environment variable using the local socket address of the incoming connection from the request context. This improves out-of-the-box compatibility for PHP applications that rely on $_SERVER['SERVER_ADDR'] (such as legacy frameworks and custom telemetry packages) when migrating from Nginx or Apache.

Co-developed-by: Gemini AI <renich+gemini@woralelandia.com>
Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>

* fix(fastcgi): validate SERVER_ADDR IP format and add test coverage

Validate that SERVER_ADDR is only populated when the local address parses as a valid IP address, preventing Unix socket paths from being assigned if SplitHostPort fails. Add test cases covering IPv4, IPv6, missing context, Unix sockets, and explicit overrides.

Co-developed-by: Gemini AI <renich+gemini@woralelandia.com>
Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>
2026-08-10 15:24:13 +10:00
Christian Mehlmauer 2adc763020 fileserver: speed up directory browsing for large directories (#7903)
* improve fileserver performance and allocations
* skip instead of fatal
* fix comment
* implement more suggestions
* use filesystems.OsFS instead of os.DirFS
* fmt
* remove outdated benchmarks
2026-08-01 00:38:04 +10:00
TowyTowyandClaude Fable 5 b2be548275 reverseproxy: fix broken reservoir sampling in random_choose policy (#7873)
The random_choose selection policy is meant to implement
power-of-d-choices: sample d available upstreams uniformly, then pick
the least-loaded of that sample. The sampling loop, however, was not a
correct reservoir sample (Algorithm R): it never filled the first k
reservoir slots unconditionally, instead writing every candidate to a
random slot j = rand(i+1), which can evict an earlier candidate while
leaving another slot nil. It also derived j from the upstream's index
in the pool rather than from the number of available upstreams seen,
skewing the sample whenever unavailable upstreams precede available
ones.

As a result the reservoir frequently held fewer than min(k, available)
upstreams, so the least-load comparison often never happened. Most
notably, with two upstreams and 'random_choose 2' (the canonical
power-of-two-choices setup), half of all requests were routed to the
more-loaded upstream even when it was saturated and the other idle --
identical behavior to plain 'random'. The nil slots this leaves behind
were also the cause of the panic reported in #3810, which was patched
by skipping nils in leastRequests rather than by fixing the sampling.

Replace the loop with a standard Algorithm R reservoir sample over the
available upstreams: fill the first k slots, then replace a random slot
with probability k/n. Every available upstream is now sampled uniformly
and the reservoir always holds min(k, available) candidates.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:25:31 +10:00
WeidiDeng 1830809afe reverseproxy: save dial info in a context key instead of a variable key to avoid race conditions when forward_auth is used (#7859) 2026-07-10 09:37:07 -06:00
TowyTowyandClaude Fable 5 945d199724 reverseproxy: fix misleading handle_response error for extra matcher args (#7869)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 05:40:23 +00:00
alhuda 75c988d118 reverseproxy: compare sticky-session cookie hash in constant time (#7853) 2026-07-10 09:16:25 +10:00
Saleh d2e0ad1e92 reverseproxy: log status 499 instead of 0 when client disconnects (#7827) 2026-06-18 13:31:02 +10:00
Dionis Ramadani 25b3eab6db Merge commit from fork
* fix(reverseproxy): hop-by-hop header fix

* test(headers): Added unit tests for hop-by-hope header behavior
2026-06-12 12:39:01 -06:00
Rhul 0f7f8e9cf6 forwardauth: error on duplicate uri subdirective (#7814) 2026-06-10 23:31:04 -04:00
Rhul 55b3397a2d reverseproxy: validate on weighted_round_robin loadbalancing policy (#7807)
* reverseproxy: validate on weighted_round_robin policy

Validate that weighted_round_robin has a non-zero total weight.
This prevents configurations such as:
    weighted_round_robin 0 0
from being accepted and causing a divide-by-zero panic during request handling.

* test: validation test on zero weight upstreams.

* test: provision called instead of totalweight setting

* reverseproxy: validate on negative upstream weights

* test: regression test on weighted_round_robin selection policy
2026-06-08 02:18:20 +10:00
WeidiDengandZen Dodd fcc7860d03 reverseproxy: replace placeholders specified for sni while using http3 (#7737)
* reverseproxy: replace placeholders specified for sni while using http3

* add test for placeholder

* reverseproxy: replace placeholders specified for sni while using http3

* add test for placeholder

* reverseproxy: test HTTP/3 SNI host placeholder

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-06-02 21:49:00 -06:00
Kévin Dunglas 3eb8e48ff0 Merge commit from fork
* feat: drop headers with underscore in their names

* feat: Caddyfile binding and tests for underscore-in-header drop

Add the `allow_underscore_in_headers` global server option, refine the
doc comment, and cover the filter end-to-end: server-level unit tests
(drop, opt-out, debug log, RFC-7230 space rejection), a fastcgi unit
test for the trimmed header name replacer, and forward_auth integration
tests for both the default-drop and opt-out paths.

* remove allow_underscore_in_headers option for now
2026-05-29 11:37:17 -06:00
WeidiDengandMatt Holt ad912569b5 reverseproxy: wraps request body to prevent closing if not read (#7719)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-05-20 17:35:40 +00:00
Eyüp Can Akman 0b265eb845 reverseproxy: Add regression test for DialInfo network override (#7758) 2026-05-20 09:43:58 -04:00
Zen Dodd 88037f1666 chore: clean up wording and typo fixes (#7745)
* chore: clean up wording and typo fixes
* chore: ASCII -> alphanumeric in lexer for heredoc marker
2026-05-20 16:36:30 +10:00
James Hartig 77e9ce7404 reverseproxy: further prevent body closes from dial errors (#7715) 2026-05-12 12:05:50 -06:00
Matthew Holt 9c78b97f9e fastcgi: Fix lint 2026-05-08 10:46:28 -06:00
Kévin Dunglas fb324331f4 Merge commit from fork
Both fallbacks in splitPos relied on golang.org/x/text/search with
search.IgnoreCase, which performs Unicode equivalence matching far beyond
ASCII case folding. Combined with the validated-ASCII guarantee on every
SplitPath entry, that fallback turned non-PHP filenames into PHP scripts:

- when the inner loop hit a non-ASCII byte and the IndexString fallback
  returned -1, the loop broke without resetting match=false, so a stale
  match=true caused a non-existent .php to be reported (PoC:
  "/name.<U+00A1>.txt").
- search.IgnoreCase folded fullwidth, mathematical and circled letters
  onto ASCII, so "/shell.<math sans-serif php>",
  "/shell.<fullwidth p>hp", "/shell.<circled php>" were all detected as
  ".php" files.

Replace the fallback with strict byte-level ASCII case-insensitive
matching: any byte >= utf8.RuneSelf in the path can never be part of a
match, since SplitPath entries are validated ASCII-only and lower-cased
in Provision(). This keeps the hot path branch-light and removes the
x/text/search dependency from the main module.

Reported against FrankenPHP as GHSA-3g8v-8r37-cgjm and
GHSA-v4h7-cj44-8fc8. The vulnerable function in this module was adapted
from the same FrankenPHP code.
2026-05-07 13:59:42 -06:00
Zen Dodd d2172bea61 chore: Fix golangci-lint 2.12.1 findings (#7690) 2026-05-07 03:40:26 -04:00
Matt Holt 4d6945769d reverseproxy: Add ability to clear dynamic upstreams cache during retries (#7662)
* reverseproxy: Add ability to clear dynamic upstreams cache during retries

This is an optional interface for dynamic upstream modules to implement if they cache results.

TODO: More documentation; this is an experiment.

* Add some godoc

* Export interface; update godoc
2026-04-28 09:16:18 -06:00
Daniil Sivak aed1af5976 reverseproxy: add lb_retry_match condition on response status (#7569) 2026-04-21 14:59:31 -04:00
Zen Dodd 4430756d5c admin: Redact sensitive request headers in API logs (#7578)
* admin: Redact sensitive request headers in API logs

* Fix govulncheck and typed atomic lint failures

* Sync Go module metadata after dependency downgrade
2026-04-17 14:56:42 -06:00
tsinglua 0722cf6fd8 chore: replace interface{} with any for modernization (#7571)
Signed-off-by: tsinglua <tsinglua@outlook.com>
2026-04-11 19:53:12 +03:00
Zen Dodd ca0ca67fbd reverseproxy: make stream copy buffer size configurable (#7627) 2026-04-10 14:49:32 -06:00
yubiuser ea4ee3ae5d reverseproxy: Fix check for header_up Host {upstream_hostport} redundancy (#7564)
* Fix check for header_up

Signed-off-by: yubiuser <github@yubiuser.dev>

* Onyl check in case commonScheme == "https"

Signed-off-by: yubiuser <github@yubiuser.dev>

* Move check after TLS transport is enabled

Signed-off-by: yubiuser <github@yubiuser.dev>

---------

Signed-off-by: yubiuser <github@yubiuser.dev>
2026-03-30 10:56:10 -06:00
Marc 62e9c05264 root: introduce down-propagating Helper.BlockState for other directives/plugins to use (#7594)
* add 'root' key to Helper.State for access in frankenphp's `php_server` directive

* clone state before passing it to child directives, but keep sharing it among sibling directives

* propagate named route state from children to parent

* use BlockState to set "root" instead

* gofmt -w .

* go fmt ./...

* here we go
2026-03-28 17:44:42 +00:00
Matt Holt e98ed6232d chore: Resolve recent CI failures (#7593) 2026-03-25 23:21:27 -06:00
Francis Lavoie 6e5e08cf58 Wire up Cause for most context cancels (#7538) 2026-03-04 17:14:52 -07:00
Tom Paulus a5e7c6e232 reverseproxy: prevent body close on dial-error retries (#7547) 2026-03-04 15:17:02 -05:00
Francis Lavoie db2986028f reverseproxy: Track dynamic upstreams, enable passive healthchecking (#7539)
* reverseproxy: Track dynamic upstreams, enable passive healthchecking

* Add tests for dynamic upstream tracking, admin endpoint, health checks
2026-03-04 15:05:26 -05:00
newklei 2dbcdefbbe forward_auth: copy_headers does not strip client-supplied identity headers (Fixes GHSA-7r4p-vjf4-gxv4) (#7545)
When using copy_headers in a forward_auth block, client-supplied headers with
the same names were not being removed before being forwarded to the backend.

This happens because PR #6608 added a MatchNot guard that skips the Set
operation when the auth service does not return a given header. That guard
prevents setting headers to empty strings, which is the correct behavior,
but it also means a client can send X-User-Id: admin in their request and
if the auth service validates the token without returning X-User-Id, Caddy
skips the Set and the client value passes through unchanged to the backend.

The fix adds an unconditional delete route for each copy_headers entry,
placed just before the existing conditional set route. The delete always runs
regardless of what the auth service returns. The conditional set still only
runs when the auth service provides that header.

The end result is:
  - Client-supplied headers are always removed
  - When the auth service returns the header, the backend gets that value
  - When the auth service does not return the header, the backend sees nothing

Existing behavior is unchanged for any deployment where the auth service
returns all of the configured copy_headers entries.

Fixes GHSA-7r4p-vjf4-gxv4
2026-03-03 23:30:49 -05:00
Paulo Henrique 88616e86e6 api: Add all in-flight requests /reverse_proxy/upstreams (Fixes #7277) (#7517)
This refactors the initial approach in PR #7281, replacing the UsagePool
with a dedicated package-level sync.Map and atomic.Int64 to track
in-flight requests without global lock contention.

It also introduces a lookup map in the admin API to fix a potential
O(n^2) iteration over upstreams, ensuring that draining upstreams
are correctly exposed across config reloads without leaking memory.

Co-authored-by: Y.Horie <u5.horie@gmail.com>

reverseproxy: optimize in-flight tracking and admin API

- Replaced sync.RWMutex with sync.Map and atomic.Int64 to avoid lock contention under high RPS.
- Introduced a lookup map in the admin API to fix a potential O(n^2) iteration over upstreams.
2026-03-03 15:14:55 -07:00
Akın Demirci 11b56c6cfc reverseproxy: Fix health_port being ignored in health checks (#7533) 2026-03-03 13:10:54 -05:00
WeidiDeng 2ab043b890 reverseproxy: query escape request urls when proxy protocol is enabled (#7537) 2026-03-02 02:04:06 -05:00
Oleksandr Redko 72eaf2583a chore: Enable modernize linter (#7519) 2026-02-26 14:01:35 -07:00
Fardjad Davari 9798f6964d caddyhttp: Avoid nil pointer dereference in proxyWrapper (#7521) 2026-02-25 04:08:41 -05:00
Mohammed Al Sahaf d7b21c6104 reverseproxy: fix tls dialing w/ proxy protocol (#7508) 2026-02-21 21:37:10 -05:00
Matt Holt 95941a71e8 chore: Add nolints to work around haywire linters (#7493)
* chore: Add nolints to work around haywire linters

* More lint wrangling
2026-02-17 16:52:54 -07:00
WeidiDeng 47f3e8f8dc use math/rand/v2 instead of math/rand (#7413) 2026-02-11 09:15:51 -07:00
XYenonandAmp 03e6e439dd reverseproxy: fix X-Forwarded-* headers for Unix socket requests (#7463)
When a request arrives via a Unix domain socket (RemoteAddr == "@"),
net.SplitHostPort fails, causing addForwardedHeaders to strip all
X-Forwarded-* headers even when the connection is trusted via
trusted_proxies_unix.

Handle Unix socket connections before parsing RemoteAddr: if untrusted,
strip headers for security; if trusted, let clientIP remain empty (no
peer IP for a Unix socket hop) and fall through to the shared header
logic, preserving the existing XFF chain without appending a spurious
entry.

Amp-Thread-ID: https://ampcode.com/threads/T-019c4225-a0ad-7283-ac56-e2c01eae1103

Co-authored-by: Amp <amp@ampcode.com>
2026-02-10 13:00:20 -07:00
Kévin Dunglas 7c28c0c07a Merge commit from fork
* fix: FastCGI split SCRIPT_NAME/PATH_INFO confusion

* fix comment
2026-02-10 11:52:36 -07:00
Francis Lavoie 2ae0f7af69 reverseproxy: Set Host to {upstream_hostport} automatically if TLS (#7454) 2026-02-09 13:06:19 -07:00
Matthew Holt 3bb22672f9 reverseproxy: Customizable dial network for SRV upstreams
By request of a sponsor
2026-02-02 11:25:51 -07:00