@marcel91200 / caddy Mirror · read-only / commits / 56ae39bd
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 06:00 UTC

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.

Islam Elsayed committed Sep 9, 2026 at 17:48 UTC 56ae39bdcc1d64661038683e30ea49cad0b0121d
2 files changed +73 -2
modules/caddyhttp/reverseproxy/active_health_test.go
+69
@@ -16,6 +16,7 @@ package reverseproxy
16
17 import (
18 "context"
19 + "io"
20 "net/http"
21 "net/http/httptest"
22 "strings"
@@ -149,3 +150,71 @@ func TestActiveHealthChecksSameAddressDifferentChecksAreIndependent(t *testing.T
150 "its health state was polluted by vhost A's health check against the same address")
151 }
152 }
153 +
154 +// TestActiveHealthCheckBodyKeepsUnknownPlaceholders is a regression test for
155 +// https://github.com/caddyserver/caddy/issues/7021: an active health check
156 +// body is user-supplied and is commonly JSON, so its braces must not be read
157 +// as placeholders and blanked. Only globals are available to this replacer, so
158 +// replacing everything sent an empty body for any JSON health_request_body.
159 +func TestActiveHealthCheckBodyKeepsUnknownPlaceholders(t *testing.T) {
160 + resetDynamicHosts()
161 + defer drainHostsPool()
162 +
163 + const jsonBody = `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}`
164 +
165 + var gotBody atomic.Value
166 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
167 + body, err := io.ReadAll(r.Body)
168 + if err != nil {
169 + t.Errorf("reading request body: %v", err)
170 + }
171 + gotBody.Store(string(body))
172 + w.WriteHeader(http.StatusOK)
173 + }))
174 + defer srv.Close()
175 + addr := strings.TrimPrefix(srv.URL, "http://")
176 +
177 + h, u, cancel := newActiveHandler(t, addr, "/health", 1)
178 + defer cancel()
179 +
180 + h.HealthChecks.Active.Body = jsonBody
181 +
182 + runActiveHealthCheck(t, h, u)
183 +
184 + if got, want := gotBody.Load(), jsonBody; got != want {
185 + t.Errorf("health check body = %q, want %q", got, want)
186 + }
187 +}
188 +
189 +// TestActiveHealthCheckBodyStillReplacesKnownPlaceholders guards the other
190 +// direction: switching to ReplaceKnown must not stop real placeholders from
191 +// being replaced.
192 +func TestActiveHealthCheckBodyStillReplacesKnownPlaceholders(t *testing.T) {
193 + resetDynamicHosts()
194 + defer drainHostsPool()
195 +
196 + t.Setenv("CADDY_TEST_HEALTH_TOKEN", "s3cret")
197 +
198 + var gotBody atomic.Value
199 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
200 + body, err := io.ReadAll(r.Body)
201 + if err != nil {
202 + t.Errorf("reading request body: %v", err)
203 + }
204 + gotBody.Store(string(body))
205 + w.WriteHeader(http.StatusOK)
206 + }))
207 + defer srv.Close()
208 + addr := strings.TrimPrefix(srv.URL, "http://")
209 +
210 + h, u, cancel := newActiveHandler(t, addr, "/health", 1)
211 + defer cancel()
212 +
213 + h.HealthChecks.Active.Body = `{"token":"{env.CADDY_TEST_HEALTH_TOKEN}"}`
214 +
215 + runActiveHealthCheck(t, h, u)
216 +
217 + if got, want := gotBody.Load(), `{"token":"s3cret"}`; got != want {
218 + t.Errorf("health check body = %q, want %q", got, want)
219 + }
220 +}
modules/caddyhttp/reverseproxy/healthchecks.go
+4 -2
@@ -430,8 +430,10 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
430 // if body is provided, create a reader for it, otherwise nil
431 var requestBody io.Reader
432 if h.HealthChecks.Active.Body != "" {
433 - // set body, using replacer
434 - requestBody = strings.NewReader(repl.ReplaceAll(h.HealthChecks.Active.Body, ""))
433 + // Set body, using replacer. Only known placeholders are replaced: the
434 + // body is often JSON, whose braces would otherwise be read as
435 + // placeholders and blanked, sending an empty body instead.
436 + requestBody = strings.NewReader(repl.ReplaceKnown(h.HealthChecks.Active.Body, ""))
437 }
438
439 // attach dialing information to this request, as well as context values that